api key and security

This commit is contained in:
salvacybersec
2025-11-13 03:40:05 +03:00
parent 763a5a0a01
commit 372ed6401b
7 changed files with 1210 additions and 21 deletions

413
API.md Normal file
View File

@@ -0,0 +1,413 @@
# API Dokümantasyonu
YouTube Transcript RSS Feed API'si, YouTube kanallarının video transcript'lerini RSS/Atom feed formatında sunar.
## Base URL
```
http://localhost:5000
```
Production için base URL değişebilir.
## Authentication
**Tüm endpoint'ler API key gerektirir.**
API key'i iki şekilde gönderebilirsiniz:
### 1. HTTP Header (Önerilen)
```http
X-API-Key: your_api_key_here
```
### 2. Query Parameter
```
?api_key=your_api_key_here
```
### API Key Alma
API key'ler `config/security.yaml` dosyasından yönetilir. Yeni bir API key eklemek için:
```yaml
security:
api_keys:
your_api_key_here:
name: "Your API Key Name"
rate_limit: 100 # Dakikada maksimum istek
enabled: true
```
### Hata Yanıtları
**401 Unauthorized** - API key eksik veya geçersiz:
```json
{
"error": "Geçersiz veya eksik API key",
"message": "X-API-Key header veya api_key query parametresi gerekli"
}
```
**429 Too Many Requests** - Rate limit aşıldı:
```json
{
"error": "Rate limit aşıldı",
"message": "Dakikada 100 istek limiti",
"retry_after": 60
}
```
## Rate Limiting
Her API key için dakikada maksimum istek sayısı tanımlanır. Rate limit bilgisi response header'ında döner:
```
X-RateLimit-Remaining: 45
```
Rate limit aşıldığında `429 Too Many Requests` hatası döner.
## Endpoints
### 1. RSS/Atom Feed Oluştur
YouTube kanalı için transcript feed'i oluşturur.
**Endpoint:** `GET /`
**Query Parameters:**
| Parametre | Tip | Zorunlu | Açıklama |
|-----------|-----|---------|----------|
| `api_key` | string | ✅ | API key (header ile de gönderilebilir) |
| `channel_id` | string | ⚠️* | YouTube Channel ID (UC ile başlar, 24 karakter) |
| `channel` | string | ⚠️* | Channel handle (@username veya username) |
| `channel_url` | string | ⚠️* | Full YouTube channel URL |
| `format` | string | ❌ | Feed formatı: `Atom` (varsayılan) veya `Rss` |
| `max_items` | integer | ❌ | Maksimum video sayısı (varsayılan: 50, max: 500) |
\* `channel_id`, `channel` veya `channel_url` parametrelerinden biri zorunludur.
**Örnek İstekler:**
```bash
# Channel ID ile
curl -H "X-API-Key: demo_key_12345" \
"http://localhost:5000/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&format=Atom"
# Channel handle ile
curl -H "X-API-Key: demo_key_12345" \
"http://localhost:5000/?channel=@tavakfi&format=Atom"
# Channel URL ile
curl -H "X-API-Key: demo_key_12345" \
"http://localhost:5000/?channel_url=https://www.youtube.com/@tavakfi&format=Atom&max_items=100"
# Query parametresi ile API key
curl "http://localhost:5000/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&api_key=demo_key_12345&format=Rss"
```
**Başarılı Yanıt:**
**Atom Format:**
```xml
<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
<id>UC9h8BDcXwkhZtnqoQJ7PggA</id>
<title>YouTube Transcript Feed - UC9h8BDcXwkhZtnqoQJ7PggA</title>
<updated>2025-01-13T00:30:36+00:00</updated>
<link href="https://www.youtube.com/channel/UC9h8BDcXwkhZtnqoQJ7PggA"/>
<entry>
<id>r5KfWUv6wqQ</id>
<title>Video Title</title>
<content type="html">
<p>Transcript content...</p>
</content>
<link href="https://www.youtube.com/watch?v=r5KfWUv6wqQ"/>
<published>2025-01-06T14:13:57+00:00</published>
</entry>
</feed>
```
**RSS Format:**
```xml
<?xml version='1.0' encoding='UTF-8'?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>YouTube Transcript Feed - UC9h8BDcXwkhZtnqoQJ7PggA</title>
<link>https://www.youtube.com/channel/UC9h8BDcXwkhZtnqoQJ7PggA</link>
<item>
<title>Video Title</title>
<content:encoded><![CDATA[<p>Transcript content...</p>]]></content:encoded>
<link>https://www.youtube.com/watch?v=r5KfWUv6wqQ</link>
<pubDate>Mon, 06 Jan 2025 14:13:57 +0000</pubDate>
</item>
</channel>
</rss>
```
**Hata Yanıtları:**
**400 Bad Request** - Geçersiz parametreler:
```json
{
"error": "Channel ID bulunamadı",
"usage": {
"channel_id": "UC... (YouTube Channel ID)",
"channel": "@username veya username",
"channel_url": "https://www.youtube.com/@username veya https://www.youtube.com/channel/UC...",
"format": "Atom veya Rss (varsayılan: Atom)",
"max_items": "Maksimum video sayısı (varsayılan: 50)"
}
}
```
**400 Bad Request** - Geçersiz format:
```json
{
"error": "Geçersiz channel_id formatı",
"message": "Channel ID UC ile başlayan 24 karakter olmalı"
}
```
**404 Not Found** - Henüz işlenmiş video yok:
```json
{
"error": "Henüz işlenmiş video yok",
"channel_id": "UC9h8BDcXwkhZtnqoQJ7PggA",
"message": "Lütfen birkaç dakika sonra tekrar deneyin"
}
```
**500 Internal Server Error** - Sunucu hatası:
```json
{
"error": "RSS-Bridge hatası: ...",
"channel_id": "UC9h8BDcXwkhZtnqoQJ7PggA"
}
```
---
### 2. Health Check
Servisin durumunu kontrol eder. **API key gerektirmez.**
**Endpoint:** `GET /health`
**Örnek İstek:**
```bash
curl "http://localhost:5000/health"
```
**Başarılı Yanıt:**
```json
{
"status": "ok",
"service": "YouTube Transcript RSS Feed"
}
```
**HTTP Status:** `200 OK`
---
### 3. API Bilgileri
API hakkında bilgi döner.
**Endpoint:** `GET /info`
**Örnek İstek:**
```bash
curl -H "X-API-Key: demo_key_12345" "http://localhost:5000/info"
```
**Başarılı Yanıt:**
```json
{
"service": "YouTube Transcript RSS Feed Generator",
"version": "1.0.0",
"endpoints": {
"/": "RSS Feed Generator",
"/health": "Health Check",
"/info": "API Info"
},
"usage": {
"channel_id": "UC... (YouTube Channel ID)",
"channel": "@username veya username",
"channel_url": "Full YouTube channel URL",
"format": "Atom veya Rss (varsayılan: Atom)",
"max_items": "Maksimum video sayısı (varsayılan: 50)"
},
"examples": [
"/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&format=Atom",
"/?channel=@tavakfi&format=Rss",
"/?channel_url=https://www.youtube.com/@tavakfi&format=Atom&max_items=100"
]
}
```
**HTTP Status:** `200 OK`
---
## Response Headers
Tüm başarılı response'larda aşağıdaki header'lar bulunur:
| Header | Açıklama |
|--------|----------|
| `X-RateLimit-Remaining` | Kalan istek sayısı |
| `X-Content-Type-Options` | `nosniff` |
| `X-Frame-Options` | `DENY` |
| `X-XSS-Protection` | `1; mode=block` |
| `Content-Type` | `application/atom+xml` veya `application/rss+xml` |
## Hata Kodları
| HTTP Status | Açıklama |
|-------------|----------|
| `200 OK` | İstek başarılı |
| `400 Bad Request` | Geçersiz parametreler |
| `401 Unauthorized` | API key eksik veya geçersiz |
| `404 Not Found` | Kaynak bulunamadı |
| `429 Too Many Requests` | Rate limit aşıldı |
| `500 Internal Server Error` | Sunucu hatası |
## Input Validation
### Channel ID Formatı
- `UC` ile başlamalı
- Toplam 24 karakter
- Alfanumerik karakterler + `_` ve `-`
**Örnek:** `UC9h8BDcXwkhZtnqoQJ7PggA`
### Channel Handle Formatı
- `@` ile başlayabilir veya başlamayabilir
- Maksimum 30 karakter
- Alfanumerik karakterler + `_` ve `-`
**Örnekler:** `@tavakfi`, `tavakfi`
### Channel URL Formatı
Sadece aşağıdaki formatlar kabul edilir:
- `https://www.youtube.com/channel/UC...`
- `https://www.youtube.com/@username`
- `https://youtube.com/channel/UC...`
- `https://youtube.com/@username`
### max_items
- Tip: Integer
- Minimum: 1
- Maksimum: 500
- Varsayılan: 50
## CORS
API CORS desteği sağlar. Preflight request'ler için `OPTIONS` metodu kullanılır.
**İzin Verilen Methodlar:** `GET`, `OPTIONS`
**İzin Verilen Header'lar:** `Content-Type`, `X-API-Key`
## Örnek Kullanım Senaryoları
### 1. RSS Reader ile Kullanım
RSS reader uygulamanızda feed URL'si olarak kullanın:
```
http://your-api.com/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&format=Rss&api_key=your_api_key
```
### 2. Programatik Kullanım (Python)
```python
import requests
api_key = "your_api_key_here"
channel_id = "UC9h8BDcXwkhZtnqoQJ7PggA"
headers = {
"X-API-Key": api_key
}
response = requests.get(
f"http://localhost:5000/?channel_id={channel_id}&format=Atom",
headers=headers
)
if response.status_code == 200:
feed_content = response.text
print(feed_content)
else:
print(f"Error: {response.status_code}")
print(response.json())
```
### 3. Programatik Kullanım (JavaScript)
```javascript
const apiKey = "your_api_key_here";
const channelId = "UC9h8BDcXwkhZtnqoQJ7PggA";
fetch(`http://localhost:5000/?channel_id=${channelId}&format=Atom`, {
headers: {
"X-API-Key": apiKey
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
.then(feedContent => {
console.log(feedContent);
})
.catch(error => {
console.error("Error:", error);
});
```
### 4. cURL ile Test
```bash
# API key ile test
API_KEY="demo_key_12345"
CHANNEL_ID="UC9h8BDcXwkhZtnqoQJ7PggA"
curl -H "X-API-Key: $API_KEY" \
"http://localhost:5000/?channel_id=$CHANNEL_ID&format=Atom&max_items=10"
```
## Notlar
1. **İlk İstek**: İlk istekte transcript'ler henüz işlenmemiş olabilir. Birkaç dakika bekleyip tekrar deneyin.
2. **Rate Limiting**: Her API key için farklı rate limit tanımlanabilir. Limit aşıldığında 60 saniye beklemeniz gerekir.
3. **Transcript İşleme**: Transcript'ler arka planda asenkron olarak işlenir. Yeni videolar için birkaç dakika gecikme olabilir.
4. **Format Seçimi**: Atom formatı daha modern ve önerilir. RSS formatı eski RSS reader'lar için uygundur.
5. **API Key Güvenliği**: API key'lerinizi güvenli tutun ve asla public repository'lere commit etmeyin.
## Destek
Sorularınız için GitHub Issues kullanabilirsiniz.

View File

@@ -6,6 +6,8 @@ YouTube video transkriptlerini otomatik olarak çıkarıp, tam metin içeren RSS
-**RSS-Bridge benzeri URL template** - Kanal adı/linki ile direkt feed -**RSS-Bridge benzeri URL template** - Kanal adı/linki ile direkt feed
-**Web Server Modu** - Flask ile RESTful API -**Web Server Modu** - Flask ile RESTful API
-**API Key Authentication** - Tüm endpoint'ler API key gerektirir
-**Güvenlik Önlemleri** - SQL injection, XSS, rate limiting koruması
- ✅ RSS-Bridge entegrasyonu (100+ video desteği) - ✅ RSS-Bridge entegrasyonu (100+ video desteği)
- ✅ Async rate limiting (AIOLimiter) - ✅ Async rate limiting (AIOLimiter)
- ✅ SpaCy ile Sentence Boundary Detection - ✅ SpaCy ile Sentence Boundary Detection
@@ -29,27 +31,41 @@ docker-compose up -d
docker-compose logs -f docker-compose logs -f
``` ```
### API Key Yapılandırması
**ÖNEMLİ:** Tüm endpoint'ler API key gerektirir!
API key'leri `config/security.yaml` dosyasından yönetin:
```yaml
security:
require_api_key: true
api_keys:
demo_key_12345:
name: "Demo API Key"
rate_limit: 100
enabled: true
```
### URL Template Kullanımı ### URL Template Kullanımı
RSS-Bridge benzeri URL template sistemi: RSS-Bridge benzeri URL template sistemi:
``` ```bash
# Channel ID ile # Channel ID ile (API key header'da)
http://localhost:5000/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&format=Atom curl -H "X-API-Key: demo_key_12345" \
"http://localhost:5000/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&format=Atom"
# Channel Handle ile # Channel Handle ile (API key query parametresi)
http://localhost:5000/?channel=@tavakfi&format=Atom curl "http://localhost:5000/?channel=@tavakfi&format=Atom&api_key=demo_key_12345"
# Channel URL ile # Channel URL ile
http://localhost:5000/?channel_url=https://www.youtube.com/@tavakfi&format=Atom curl -H "X-API-Key: demo_key_12345" \
"http://localhost:5000/?channel_url=https://www.youtube.com/@tavakfi&format=Atom&max_items=100"
# RSS formatı
http://localhost:5000/?channel=@tavakfi&format=Rss
# Maksimum video sayısı
http://localhost:5000/?channel=@tavakfi&format=Atom&max_items=100
``` ```
**Detaylı API dokümantasyonu için:** [API.md](API.md)
### Batch Mode (Manuel Çalıştırma) ### Batch Mode (Manuel Çalıştırma)
```bash ```bash
@@ -78,6 +94,8 @@ python main.py
## Yapılandırma ## Yapılandırma
### Ana Yapılandırma
`config/config.yaml` dosyasını düzenleyin: `config/config.yaml` dosyasını düzenleyin:
```yaml ```yaml
@@ -92,6 +110,23 @@ rss_bridge:
max_items: 100 max_items: 100
``` ```
### Güvenlik Yapılandırması
`config/security.yaml` dosyasından API key'leri ve güvenlik ayarlarını yönetin:
```yaml
security:
require_api_key: true
api_keys:
your_api_key_here:
name: "Your API Key"
rate_limit: 100
enabled: true
default_rate_limit: 60
```
**Detaylı güvenlik dokümantasyonu için:** [SECURITY.md](SECURITY.md)
## Proje Yapısı ## Proje Yapısı
``` ```
@@ -113,6 +148,18 @@ yttranscriptrss/
└── main.py └── main.py
``` ```
## Dokümantasyon
- **[API.md](API.md)** - Detaylı API dokümantasyonu, endpoint'ler, örnekler
- **[SECURITY.md](SECURITY.md)** - Güvenlik önlemleri, best practices
- **[development_plan.md](development_plan.md)** - Geliştirme planı ve roadmap
## Endpoints
- `GET /` - RSS/Atom feed oluştur (API key gerekli)
- `GET /health` - Health check (API key gerekmez)
- `GET /info` - API bilgileri (API key gerekli)
## Geliştirme Planı ## Geliştirme Planı
Detaylı geliştirme planı için `development_plan.md` dosyasına bakın. Detaylı geliştirme planı için `development_plan.md` dosyasına bakın.

212
SECURITY.md Normal file
View File

@@ -0,0 +1,212 @@
# Güvenlik Dokümantasyonu
Bu dokümantasyon, YouTube Transcript RSS Feed projesinin güvenlik önlemlerini açıklar.
## Güvenlik Özellikleri
### 1. SQL Injection Koruması
- **Parametrize Queries**: Tüm SQL sorguları parametrize edilmiş query'ler kullanır
- **Input Validation**: Veritabanına giden tüm inputlar format kontrolünden geçer
- **Video ID Validation**: YouTube video ID formatı kontrol edilir (11 karakter, alfanumerik)
- **Channel ID Validation**: YouTube channel ID formatı kontrol edilir (UC ile başlayan 24 karakter)
**Örnek:**
```python
# Güvenli (parametrize query)
cursor.execute("SELECT * FROM videos WHERE video_id = ?", (video_id,))
# Güvensiz (kullanılmıyor!)
# cursor.execute(f"SELECT * FROM videos WHERE video_id = '{video_id}'")
```
### 2. XSS (Cross-Site Scripting) Koruması
- **Input Sanitization**: Tüm kullanıcı inputları sanitize edilir
- **HTML Tag Removal**: Tehlikeli HTML tag'leri kaldırılır
- **Script Tag Removal**: `<script>` tag'leri ve JavaScript event handler'ları temizlenir
- **Length Limits**: Input uzunlukları sınırlandırılır
**Özellikler:**
- HTML tag'leri kaldırılır: `<script>`, `<iframe>`, vb.
- JavaScript event handler'ları temizlenir: `onclick=`, `onerror=`, vb.
- Maksimum uzunluk sınırları: channel_id (50), channel_url (200), vb.
### 3. API Key Authentication (Opsiyonel)
API key sistemi ile endpoint'lere erişim kontrolü sağlanabilir.
**Yapılandırma:**
```yaml
# config/security.yaml
security:
require_api_key: false # true yaparak zorunlu hale getir
api_keys:
your_api_key_here:
name: "My API Key"
rate_limit: 100 # Dakikada maksimum istek
enabled: true
```
**Kullanım:**
```bash
# Header ile
curl -H "X-API-Key: your_api_key_here" http://localhost:5000/
# Query parametresi ile
curl "http://localhost:5000/?channel_id=UC...&api_key=your_api_key_here"
```
### 4. Rate Limiting
- **IP Bazlı Rate Limiting**: IP adresine göre istek limiti
- **API Key Bazlı Rate Limiting**: API key'e özel rate limit
- **Endpoint Bazlı Limitler**: Her endpoint için farklı limitler
**Varsayılan Limitler:**
- Ana feed endpoint (`/`): 60 istek/dakika
- Health check (`/health`): 120 istek/dakika
- Info endpoint (`/info`): 120 istek/dakika
**Rate Limit Header'ları:**
```
X-RateLimit-Remaining: 45
```
**Rate Limit Aşıldığında:**
```json
{
"error": "Rate limit aşıldı",
"message": "Dakikada 60 istek limiti",
"retry_after": 60
}
```
HTTP Status: `429 Too Many Requests`
### 5. Input Validation
Tüm endpoint'lerde input validation yapılır:
- **Channel ID**: `UC` ile başlayan 24 karakter
- **Channel Handle**: Alfanumerik karakterler, maksimum 30 karakter
- **Channel URL**: Sadece YouTube URL'lerine izin verilir
- **max_items**: 1-500 arası sayı
**Geçersiz Input Örneği:**
```json
{
"error": "Geçersiz channel_id formatı",
"message": "Channel ID UC ile başlayan 24 karakter olmalı"
}
```
### 6. CORS (Cross-Origin Resource Sharing)
CORS ayarları `config/security.yaml` dosyasından yapılandırılabilir.
**Varsayılan:**
```yaml
cors:
enabled: true
allowed_origins: ["*"] # Production'da spesifik domain'ler belirtilmeli
allowed_methods: ["GET", "OPTIONS"]
allowed_headers: ["Content-Type", "X-API-Key"]
```
### 7. Security Headers
Tüm response'lara güvenlik header'ları eklenir:
- `X-Content-Type-Options: nosniff`
- `X-Frame-Options: DENY`
- `X-XSS-Protection: 1; mode=block`
- `Strict-Transport-Security: max-age=31536000; includeSubDomains`
- `Content-Security-Policy: default-src 'self'`
## Güvenlik Best Practices
### 1. API Key Yönetimi
- **Production'da mutlaka değiştirin**: `config/security.yaml` dosyasındaki demo key'leri değiştirin
- **Güçlü key'ler kullanın**: En az 32 karakter, rastgele oluşturulmuş
- **Key rotation**: Düzenli olarak API key'leri değiştirin
- **Key'leri güvenli saklayın**: Git'e commit etmeyin, environment variable kullanın
### 2. Rate Limiting
- **Production için uygun limitler belirleyin**: Trafiğe göre ayarlayın
- **API key bazlı limitler**: Farklı kullanıcılar için farklı limitler
- **Monitoring**: Rate limit aşımlarını loglayın
### 3. Input Validation
- **Her zaman validate edin**: Tüm kullanıcı inputlarını validate edin
- **Whitelist yaklaşımı**: Sadece izin verilen formatları kabul edin
- **Length limits**: Maksimum uzunluk sınırları koyun
### 4. SQL Injection
- **Parametrize queries**: Her zaman parametrize query kullanın
- **Input validation**: Veritabanına giden tüm inputları validate edin
- **Error handling**: Hata mesajlarında hassas bilgi göstermeyin
### 5. XSS Koruması
- **Output encoding**: Feedgen otomatik encoding yapar
- **Input sanitization**: Tüm inputları sanitize edin
- **Content Security Policy**: CSP header'ları kullanın
## Güvenlik Yapılandırması
Güvenlik ayarları `config/security.yaml` dosyasından yönetilir:
```yaml
security:
require_api_key: false
api_keys:
your_key:
name: "Key Name"
rate_limit: 100
enabled: true
default_rate_limit: 60
cors:
enabled: true
allowed_origins: ["*"]
security_headers:
X-Content-Type-Options: "nosniff"
# ...
```
## Güvenlik Testleri
### 1. SQL Injection Testi
```bash
# Bu istek başarısız olmalı (validation hatası)
curl "http://localhost:5000/?channel_id=' OR '1'='1"
```
### 2. XSS Testi
```bash
# Bu istek sanitize edilmeli
curl "http://localhost:5000/?channel_id=<script>alert('XSS')</script>"
```
### 3. Rate Limiting Testi
```bash
# 60+ istek gönder, 429 hatası almalı
for i in {1..70}; do curl "http://localhost:5000/?channel_id=UC..."; done
```
## Güvenlik Güncellemeleri
- Düzenli olarak bağımlılıkları güncelleyin: `pip install --upgrade`
- Güvenlik açıklarını takip edin: GitHub Security Advisories
- Log'ları izleyin: Şüpheli aktiviteleri tespit edin
## İletişim
Güvenlik açığı bulursanız, lütfen sorumlu disclosure yapın.

53
config/security.yaml Normal file
View File

@@ -0,0 +1,53 @@
# Güvenlik ayarları
security:
# API Key Authentication
require_api_key: true # true ise tüm endpoint'ler API key gerektirir
api_keys:
# API key'ler: key -> {name, rate_limit, enabled}
# Örnek API key'ler (production'da değiştirilmeli!)
demo_key_12345:
name: "Demo API Key"
rate_limit: 100 # Dakikada maksimum istek
enabled: true
created_at: "2025-01-01"
# Daha fazla API key eklenebilir
# production_key_xyz:
# name: "Production Key"
# rate_limit: 1000
# enabled: true
# Rate Limiting (IP bazlı, API key yoksa)
default_rate_limit: 60 # Dakikada maksimum istek
rate_limit_by_endpoint:
"/": 60 # Ana feed endpoint
"/health": 120 # Health check daha fazla izin ver
"/info": 120 # Info endpoint
# Input Validation
max_input_length:
channel_id: 50
channel_handle: 50
channel_url: 200
max_items: 500
# CORS Settings
cors:
enabled: true
allowed_origins:
- "*" # Production'da spesifik domain'ler belirtilmeli
allowed_methods:
- "GET"
- "OPTIONS"
allowed_headers:
- "Content-Type"
- "X-API-Key"
# Security Headers
security_headers:
X-Content-Type-Options: "nosniff"
X-Frame-Options: "DENY"
X-XSS-Protection: "1; mode=block"
Strict-Transport-Security: "max-age=31536000; includeSubDomains" # HTTPS için
Content-Security-Policy: "default-src 'self'"

View File

@@ -3,6 +3,7 @@ SQLite veritabanı yönetimi modülü
""" """
import sqlite3 import sqlite3
import os import os
import re
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional, List, Dict from typing import Optional, List, Dict
@@ -89,8 +90,24 @@ class Database:
if self.conn: if self.conn:
self.conn.close() self.conn.close()
def _validate_video_id(self, video_id: str) -> bool:
"""Video ID formatını doğrula (SQL injection koruması)"""
if not video_id or len(video_id) > 20:
return False
# YouTube video ID: 11 karakter, alfanumerik + _ -
return bool(re.match(r'^[a-zA-Z0-9_-]{11}$', video_id))
def _validate_channel_id(self, channel_id: str) -> bool:
"""Channel ID formatını doğrula (SQL injection koruması)"""
if not channel_id or len(channel_id) > 50:
return False
# YouTube channel ID: UC ile başlayan 24 karakter
return bool(re.match(r'^UC[a-zA-Z0-9_-]{22}$', channel_id))
def is_video_processed(self, video_id: str) -> bool: def is_video_processed(self, video_id: str) -> bool:
"""Video işlenmiş mi kontrol et""" """Video işlenmiş mi kontrol et"""
if not self._validate_video_id(video_id):
raise ValueError(f"Geçersiz video_id formatı: {video_id}")
cursor = self.conn.cursor() cursor = self.conn.cursor()
cursor.execute("SELECT video_id FROM videos WHERE video_id = ?", (video_id,)) cursor.execute("SELECT video_id FROM videos WHERE video_id = ?", (video_id,))
return cursor.fetchone() is not None return cursor.fetchone() is not None
@@ -107,16 +124,26 @@ class Database:
def add_video(self, video_data: Dict): def add_video(self, video_data: Dict):
"""Yeni video ekle (status=0 olarak)""" """Yeni video ekle (status=0 olarak)"""
# Input validation
video_id = video_data.get('video_id')
channel_id = video_data.get('channel_id')
if not self._validate_video_id(video_id):
raise ValueError(f"Geçersiz video_id formatı: {video_id}")
if channel_id and not self._validate_channel_id(channel_id):
raise ValueError(f"Geçersiz channel_id formatı: {channel_id}")
cursor = self.conn.cursor() cursor = self.conn.cursor()
cursor.execute(""" cursor.execute("""
INSERT OR IGNORE INTO videos INSERT OR IGNORE INTO videos
(video_id, channel_id, video_title, video_url, published_at_utc, transcript_status) (video_id, channel_id, video_title, video_url, published_at_utc, transcript_status)
VALUES (?, ?, ?, ?, ?, 0) VALUES (?, ?, ?, ?, ?, 0)
""", ( """, (
video_data['video_id'], video_id,
video_data.get('channel_id'), channel_id,
video_data.get('video_title'), video_data.get('video_title', '')[:500], # Max length
video_data.get('video_url'), video_data.get('video_url', '')[:500], # Max length
video_data.get('published_at_utc') video_data.get('published_at_utc')
)) ))
self.conn.commit() self.conn.commit()
@@ -124,6 +151,13 @@ class Database:
def update_video_transcript(self, video_id: str, raw: str, clean: str, def update_video_transcript(self, video_id: str, raw: str, clean: str,
status: int, language: Optional[str] = None): status: int, language: Optional[str] = None):
"""Video transcript'ini güncelle""" """Video transcript'ini güncelle"""
# Input validation
if not self._validate_video_id(video_id):
raise ValueError(f"Geçersiz video_id formatı: {video_id}")
if status not in [0, 1, 2]:
raise ValueError(f"Geçersiz status değeri: {status}")
cursor = self.conn.cursor() cursor = self.conn.cursor()
now_utc = datetime.now(timezone.utc).isoformat() now_utc = datetime.now(timezone.utc).isoformat()
cursor.execute(""" cursor.execute("""
@@ -141,6 +175,13 @@ class Database:
def get_processed_videos(self, limit: Optional[int] = None, def get_processed_videos(self, limit: Optional[int] = None,
channel_id: Optional[str] = None) -> List[Dict]: channel_id: Optional[str] = None) -> List[Dict]:
"""İşlenmiş videoları getir (status=1)""" """İşlenmiş videoları getir (status=1)"""
# Input validation
if channel_id and not self._validate_channel_id(channel_id):
raise ValueError(f"Geçersiz channel_id formatı: {channel_id}")
if limit is not None and (not isinstance(limit, int) or limit < 1 or limit > 1000):
raise ValueError(f"Geçersiz limit değeri: {limit} (1-1000 arası olmalı)")
cursor = self.conn.cursor() cursor = self.conn.cursor()
query = """ query = """
SELECT * FROM videos SELECT * FROM videos
@@ -163,6 +204,10 @@ class Database:
def mark_video_failed(self, video_id: str, reason: Optional[str] = None): def mark_video_failed(self, video_id: str, reason: Optional[str] = None):
"""Video'yu başarısız olarak işaretle (status=2)""" """Video'yu başarısız olarak işaretle (status=2)"""
# Input validation
if not self._validate_video_id(video_id):
raise ValueError(f"Geçersiz video_id formatı: {video_id}")
cursor = self.conn.cursor() cursor = self.conn.cursor()
cursor.execute(""" cursor.execute("""
UPDATE videos UPDATE videos

334
src/security.py Normal file
View File

@@ -0,0 +1,334 @@
"""
Güvenlik modülü: Authentication, Rate Limiting, Input Validation
"""
import re
import hashlib
import hmac
import time
from functools import wraps
from flask import request, jsonify, g
from typing import Optional, Dict, List
from collections import defaultdict
from datetime import datetime, timedelta
class SecurityManager:
"""Güvenlik yönetim sınıfı"""
def __init__(self, api_keys: Dict[str, Dict] = None, rate_limit_per_minute: int = 60):
"""
Args:
api_keys: API key dict {key: {name, rate_limit, enabled}}
rate_limit_per_minute: Varsayılan rate limit
"""
self.api_keys = api_keys or {}
self.rate_limit_per_minute = rate_limit_per_minute
# Rate limiting için: {ip_or_key: [(timestamp, ...)]}
self.rate_limit_store = defaultdict(list)
# IP bazlı rate limiting
self.ip_rate_limits = defaultdict(list)
def validate_api_key(self, api_key: Optional[str]) -> tuple[bool, Optional[Dict]]:
"""
API key doğrula
Returns:
(is_valid, key_info) tuple
"""
if not api_key:
return False, None
# API key'i hash'le ve karşılaştır
key_info = self.api_keys.get(api_key)
if key_info and key_info.get('enabled', True):
return True, key_info
return False, None
def check_rate_limit(self, identifier: str, limit: int = None) -> tuple[bool, int]:
"""
Rate limit kontrolü
Args:
identifier: IP adresi veya API key
limit: Rate limit (None ise varsayılan kullanılır)
Returns:
(is_allowed, remaining_requests) tuple
"""
if limit is None:
limit = self.rate_limit_per_minute
now = time.time()
minute_ago = now - 60
# Eski kayıtları temizle
self.rate_limit_store[identifier] = [
ts for ts in self.rate_limit_store[identifier] if ts > minute_ago
]
# Rate limit kontrolü
if len(self.rate_limit_store[identifier]) >= limit:
remaining = 0
return False, remaining
# Yeni isteği kaydet
self.rate_limit_store[identifier].append(now)
remaining = limit - len(self.rate_limit_store[identifier])
return True, remaining
def sanitize_input(self, text: str, max_length: int = 500) -> str:
"""
Input sanitization (XSS koruması)
Args:
text: Temizlenecek metin
max_length: Maksimum uzunluk
Returns:
Temizlenmiş metin
"""
if not text:
return ""
# Uzunluk kontrolü
text = text[:max_length]
# Tehlikeli karakterleri temizle
# HTML tag'lerini kaldır (basit)
text = re.sub(r'<[^>]+>', '', text)
# Script tag'lerini kaldır
text = re.sub(r'<script[^>]*>.*?</script>', '', text, flags=re.IGNORECASE | re.DOTALL)
# JavaScript event handler'ları kaldır
text = re.sub(r'on\w+\s*=', '', text, flags=re.IGNORECASE)
return text.strip()
def validate_channel_id(self, channel_id: str) -> bool:
"""
Channel ID formatını doğrula
Args:
channel_id: YouTube Channel ID
Returns:
Geçerli mi?
"""
if not channel_id:
return False
# UC ile başlayan 24 karakter
if re.match(r'^UC[a-zA-Z0-9_-]{22}$', channel_id):
return True
return False
def validate_channel_handle(self, handle: str) -> bool:
"""
Channel handle formatını doğrula
Args:
handle: @username veya username
Returns:
Geçerli mi?
"""
if not handle:
return False
# @ ile başlayan veya başlamayan, alfanumerik + _ - karakterler
handle = handle.lstrip('@')
if re.match(r'^[a-zA-Z0-9_-]{1,30}$', handle):
return True
return False
def validate_url(self, url: str) -> bool:
"""
URL formatını doğrula
Args:
url: URL string
Returns:
Geçerli mi?
"""
if not url:
return False
# Sadece YouTube URL'lerine izin ver
youtube_patterns = [
r'^https?://(www\.)?youtube\.com/channel/[a-zA-Z0-9_-]+',
r'^https?://(www\.)?youtube\.com/@[a-zA-Z0-9_-]+',
r'^https?://(www\.)?youtu\.be/[a-zA-Z0-9_-]+',
]
for pattern in youtube_patterns:
if re.match(pattern, url):
return True
return False
def validate_max_items(self, max_items: int) -> bool:
"""
max_items parametresini doğrula
Args:
max_items: Maksimum item sayısı
Returns:
Geçerli mi?
"""
return isinstance(max_items, int) and 1 <= max_items <= 500
# Global security manager instance
_security_manager = None
def init_security(api_keys: Dict[str, Dict] = None, rate_limit: int = 60):
"""Security manager'ı initialize et"""
global _security_manager
_security_manager = SecurityManager(api_keys, rate_limit)
return _security_manager
def get_security_manager() -> SecurityManager:
"""Security manager instance'ı al"""
global _security_manager
if _security_manager is None:
_security_manager = SecurityManager()
return _security_manager
def require_api_key(f):
"""API key gerektiren decorator"""
@wraps(f)
def decorated_function(*args, **kwargs):
security = get_security_manager()
# API key'i header'dan veya query'den al
api_key = request.headers.get('X-API-Key') or request.args.get('api_key')
is_valid, key_info = security.validate_api_key(api_key)
if not is_valid:
return jsonify({
'error': 'Geçersiz veya eksik API key',
'message': 'X-API-Key header veya api_key query parametresi gerekli'
}), 401
# Rate limit kontrolü (API key bazlı)
key_rate_limit = key_info.get('rate_limit', security.rate_limit_per_minute)
is_allowed, remaining = security.check_rate_limit(api_key, key_rate_limit)
if not is_allowed:
return jsonify({
'error': 'Rate limit aşıldı',
'message': f'Dakikada {key_rate_limit} istek limiti',
'retry_after': 60
}), 429
# API key bilgisini g context'e ekle
g.api_key = api_key
g.api_key_info = key_info
# Response header'a remaining ekle
g.rate_limit_remaining = remaining
return f(*args, **kwargs)
return decorated_function
def rate_limit(limit_per_minute: int = 60):
"""Rate limiting decorator (IP bazlı)"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
security = get_security_manager()
# IP adresini al
ip_address = request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr)
if ip_address:
ip_address = ip_address.split(',')[0].strip()
# Rate limit kontrolü
is_allowed, remaining = security.check_rate_limit(ip_address, limit_per_minute)
if not is_allowed:
return jsonify({
'error': 'Rate limit aşıldı',
'message': f'Dakikada {limit_per_minute} istek limiti',
'retry_after': 60
}), 429
g.rate_limit_remaining = remaining
return f(*args, **kwargs)
return decorated_function
return decorator
def validate_input(f):
"""Input validation decorator"""
@wraps(f)
def decorated_function(*args, **kwargs):
security = get_security_manager()
# Query parametrelerini validate et
channel_id = request.args.get('channel_id')
channel = request.args.get('channel')
channel_url = request.args.get('channel_url')
max_items = request.args.get('max_items', '50')
# Channel ID validation
if channel_id and not security.validate_channel_id(channel_id):
return jsonify({
'error': 'Geçersiz channel_id formatı',
'message': 'Channel ID UC ile başlayan 24 karakter olmalı'
}), 400
# Channel handle validation
if channel and not security.validate_channel_handle(channel):
return jsonify({
'error': 'Geçersiz channel handle formatı',
'message': 'Channel handle alfanumerik karakterler içermeli (maks 30 karakter)'
}), 400
# Channel URL validation
if channel_url and not security.validate_url(channel_url):
return jsonify({
'error': 'Geçersiz channel_url formatı',
'message': 'Sadece YouTube URL\'lerine izin verilir'
}), 400
# max_items validation
try:
max_items_int = int(max_items)
if not security.validate_max_items(max_items_int):
return jsonify({
'error': 'Geçersiz max_items değeri',
'message': 'max_items 1-500 arasında olmalı'
}), 400
except ValueError:
return jsonify({
'error': 'Geçersiz max_items formatı',
'message': 'max_items sayı olmalı'
}), 400
# Input sanitization
if channel_id:
channel_id = security.sanitize_input(channel_id, max_length=50)
if channel:
channel = security.sanitize_input(channel, max_length=50)
if channel_url:
channel_url = security.sanitize_input(channel_url, max_length=200)
return f(*args, **kwargs)
return decorated_function

View File

@@ -1,9 +1,11 @@
""" """
Flask web server - RSS-Bridge benzeri URL template sistemi Flask web server - RSS-Bridge benzeri URL template sistemi
""" """
from flask import Flask, request, Response, jsonify from flask import Flask, request, Response, jsonify, g, after_request
from typing import Optional from typing import Optional
import sys import sys
import os
import yaml
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent)) sys.path.insert(0, str(Path(__file__).parent.parent))
@@ -13,10 +15,80 @@ from src.video_fetcher import fetch_videos_from_rss_bridge, get_channel_id_from_
from src.transcript_extractor import TranscriptExtractor from src.transcript_extractor import TranscriptExtractor
from src.transcript_cleaner import TranscriptCleaner from src.transcript_cleaner import TranscriptCleaner
from src.rss_generator import RSSGenerator from src.rss_generator import RSSGenerator
from src.security import (
init_security, get_security_manager,
require_api_key, rate_limit, validate_input
)
app = Flask(__name__) app = Flask(__name__)
# Security config yükle
_security_config = None
def load_security_config():
"""Security config'i yükle"""
global _security_config
if _security_config is None:
config_path = Path(__file__).parent.parent / 'config' / 'security.yaml'
if config_path.exists():
with open(config_path, 'r', encoding='utf-8') as f:
_security_config = yaml.safe_load(f).get('security', {})
else:
_security_config = {}
return _security_config
# Security manager'ı initialize et
def init_app_security():
"""Security manager'ı uygulama başlangıcında initialize et"""
config = load_security_config()
api_keys = config.get('api_keys', {})
default_rate_limit = config.get('default_rate_limit', 60)
init_security(api_keys, default_rate_limit)
# Security headers ve CORS middleware
@app.after_request
def add_security_headers(response):
"""Security header'ları ekle"""
config = load_security_config()
headers = config.get('security_headers', {})
for header, value in headers.items():
response.headers[header] = value
# CORS headers
cors_config = config.get('cors', {})
if cors_config.get('enabled', True):
origins = cors_config.get('allowed_origins', ['*'])
if '*' in origins:
response.headers['Access-Control-Allow-Origin'] = '*'
else:
origin = request.headers.get('Origin')
if origin in origins:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Methods'] = ', '.join(
cors_config.get('allowed_methods', ['GET', 'OPTIONS'])
)
response.headers['Access-Control-Allow-Headers'] = ', '.join(
cors_config.get('allowed_headers', ['Content-Type', 'X-API-Key'])
)
# Rate limit bilgisini header'a ekle
if hasattr(g, 'rate_limit_remaining'):
response.headers['X-RateLimit-Remaining'] = str(g.rate_limit_remaining)
return response
# OPTIONS handler for CORS
@app.route('/', methods=['OPTIONS'])
@app.route('/<path:path>', methods=['OPTIONS'])
def handle_options(path=None):
"""CORS preflight request handler"""
return Response(status=200)
# Uygulama başlangıcında security'yi initialize et
init_app_security()
# Global instances (lazy loading) # Global instances (lazy loading)
db = None db = None
extractor = None extractor = None
@@ -165,6 +237,8 @@ def process_channel(channel_id: str, max_items: int = 50) -> dict:
@app.route('/', methods=['GET']) @app.route('/', methods=['GET'])
@require_api_key # API key zorunlu
@validate_input # Input validation
def generate_feed(): def generate_feed():
""" """
RSS-Bridge benzeri URL template: RSS-Bridge benzeri URL template:
@@ -174,12 +248,15 @@ def generate_feed():
- /?channel=@tavakfi&format=Atom - /?channel=@tavakfi&format=Atom
- /?channel_url=https://www.youtube.com/@tavakfi&format=Atom - /?channel_url=https://www.youtube.com/@tavakfi&format=Atom
""" """
# Query parametrelerini al # Query parametrelerini al (validate_input decorator zaten sanitize etti)
channel_id = request.args.get('channel_id') channel_id = request.args.get('channel_id')
channel = request.args.get('channel') # @username veya username channel = request.args.get('channel') # @username veya username
channel_url = request.args.get('channel_url') channel_url = request.args.get('channel_url')
format_type = request.args.get('format', 'Atom').lower() # Atom veya Rss format_type = request.args.get('format', 'Atom').lower() # Atom veya Rss
try:
max_items = int(request.args.get('max_items', 50)) max_items = int(request.args.get('max_items', 50))
except (ValueError, TypeError):
max_items = 50
# Channel ID'yi normalize et # Channel ID'yi normalize et
normalized_channel_id = normalize_channel_id( normalized_channel_id = normalize_channel_id(
@@ -226,20 +303,26 @@ def generate_feed():
generator.add_video_entry(video) generator.add_video_entry(video)
# Format'a göre döndür # Format'a göre döndür
response_headers = {}
if hasattr(g, 'rate_limit_remaining'):
response_headers['X-RateLimit-Remaining'] = str(g.rate_limit_remaining)
if format_type == 'rss': if format_type == 'rss':
rss_content = generator.generate_rss_string() rss_content = generator.generate_rss_string()
response_headers['Content-Type'] = 'application/rss+xml; charset=utf-8'
return Response( return Response(
rss_content, rss_content,
mimetype='application/rss+xml', mimetype='application/rss+xml',
headers={'Content-Type': 'application/rss+xml; charset=utf-8'} headers=response_headers
) )
else: # Atom else: # Atom
# Feedgen Atom desteği # Feedgen Atom desteği
atom_content = generator.generate_atom_string() atom_content = generator.generate_atom_string()
response_headers['Content-Type'] = 'application/atom+xml; charset=utf-8'
return Response( return Response(
atom_content, atom_content,
mimetype='application/atom+xml', mimetype='application/atom+xml',
headers={'Content-Type': 'application/atom+xml; charset=utf-8'} headers=response_headers
) )
except Exception as e: except Exception as e:
@@ -250,12 +333,14 @@ def generate_feed():
@app.route('/health', methods=['GET']) @app.route('/health', methods=['GET'])
@rate_limit(limit_per_minute=120) # Health check için daha yüksek limit
def health(): def health():
"""Health check endpoint""" """Health check endpoint"""
return jsonify({'status': 'ok', 'service': 'YouTube Transcript RSS Feed'}) return jsonify({'status': 'ok', 'service': 'YouTube Transcript RSS Feed'})
@app.route('/info', methods=['GET']) @app.route('/info', methods=['GET'])
@require_api_key # API key zorunlu
def info(): def info():
"""API bilgileri""" """API bilgileri"""
return jsonify({ return jsonify({