Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
.Python
*.egg-info/
dist/
build/
.venv/
venv/
env/

# Node / WhatsApp bot
node_modules/
.wwebjs_auth/
.wwebjs_cache/
npm-debug.log*

# Database & uploads
chat_history.db
chat_history.db-wal
chat_history.db-shm
uploads/
Sandbox/

# OS / Editor
.DS_Store
Thumbs.db
*.swp
*.swo
.idea/
.vscode/
137 changes: 109 additions & 28 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@
# IMPORTS
# ═══════════════════════════════════════════════════════════════
import argparse
import ast as _ast
import atexit
import base64 as b64mod
import glob
import hashlib
import io
import json
import logging
import operator as _operator
import os
import queue
import signal
Expand Down Expand Up @@ -129,6 +131,88 @@
if g.strip()
]

# ═══════════════════════════════════════════════════════════════
# GÜVENLİ HESAP MAKİNESİ — AST Tabanlı (eval() kullanılmaz)
# ═══════════════════════════════════════════════════════════════

def _safe_calc_eval(expr_str: str):
"""
AST tabanlı güvenli matematik değerlendirici.
eval() kullanmaz; yalnızca sayılar, aritmetik ve math
fonksiyonlarına izin verir.
"""
import math

_ALLOWED_NODES = (
_ast.Expression, _ast.Constant,
_ast.BinOp, _ast.UnaryOp, _ast.Call,
_ast.Add, _ast.Sub, _ast.Mult, _ast.Div,
_ast.Pow, _ast.Mod, _ast.FloorDiv,
_ast.USub, _ast.UAdd,
_ast.Name, _ast.Load,
)

_OPS = {
_ast.Add: _operator.add,
_ast.Sub: _operator.sub,
_ast.Mult: _operator.mul,
_ast.Div: _operator.truediv,
_ast.Pow: _operator.pow,
_ast.Mod: _operator.mod,
_ast.FloorDiv: _operator.floordiv,
_ast.USub: _operator.neg,
_ast.UAdd: _operator.pos,
}

_NAMES: Dict[str, Any] = {
k: v for k, v in math.__dict__.items() if not k.startswith("__")
}
for _fn in (abs, round, min, max, sum, int, float):
_NAMES[_fn.__name__] = _fn

Comment on lines +167 to +172

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_safe_calc_eval whitelists all math module symbols (including expensive functions like factorial, comb, etc.) and allows ** on unbounded integers. Inputs like factorial(100000) or 10**10000000 can cause CPU/memory exhaustion (DoS) even though code execution is blocked. Consider restricting the allowed function set to a small curated list and enforcing limits (e.g., max integer bit-length / max exponent / max factorial argument / max AST nodes).

Copilot uses AI. Check for mistakes.
def _eval(node):
if not isinstance(node, _ALLOWED_NODES):
raise ValueError(f"İzin verilmeyen ifade türü: {type(node).__name__}")
if isinstance(node, _ast.Expression):
return _eval(node.body)
if isinstance(node, _ast.Constant):
if not isinstance(node.value, (int, float, complex)):
raise ValueError("Sadece sayısal sabitler desteklenir")
return node.value
if isinstance(node, _ast.BinOp):
op = _OPS.get(type(node.op))
if op is None:
raise ValueError(f"İzin verilmeyen operatör: {type(node.op).__name__}")
return op(_eval(node.left), _eval(node.right))
if isinstance(node, _ast.UnaryOp):
op = _OPS.get(type(node.op))
if op is None:
raise ValueError(f"İzin verilmeyen tekli operatör: {type(node.op).__name__}")
return op(_eval(node.operand))
if isinstance(node, _ast.Call):
if not isinstance(node.func, _ast.Name):
raise ValueError("Yalnızca basit fonksiyon çağrıları izinli")
func = _NAMES.get(node.func.id)
if func is None:
raise ValueError(f"İzin verilmeyen fonksiyon: {node.func.id}")
args = [_eval(a) for a in node.args]
if node.keywords:
raise ValueError("Anahtar kelime argümanları desteklenmiyor")
return func(*args)
if isinstance(node, _ast.Name):
val = _NAMES.get(node.id)
if val is None:
raise ValueError(f"İzin verilmeyen değişken: {node.id}")
return val
raise ValueError(f"Desteklenmeyen düğüm: {type(node).__name__}")

try:
tree = _ast.parse(expr_str.strip(), mode='eval')
except SyntaxError as exc:
raise ValueError(f"Sözdizimi hatası: {exc}") from exc
return _eval(tree)


# ═══════════════════════════════════════════════════════════════
# KATMAN 1 — LRU RAM Cache (thread-safe, TTL, metriklere sahip)
# ═══════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -251,7 +335,11 @@ def _decode(data: bytes) -> str:
return payload.decode("utf-8")
except Exception as e:
log.warning(f"_decode hatası (len={len(data)}): {e}")
return data.decode("utf-8", errors="replace")
# Flag baytını atla; sadece payload kısmını çöz
try:
return data[1:].decode("utf-8", errors="replace") if len(data) > 1 else ""
except Exception:
return ""


def _compress_ratio(data: bytes) -> float:
Expand Down Expand Up @@ -883,7 +971,9 @@ def _make_thumbnail(self, raw: bytes, mime: str,
from PIL import Image
import io
img = Image.open(io.BytesIO(raw))
img.thumbnail(size, Image.LANCZOS)
# Pillow 10+ uses Image.Resampling.LANCZOS; older versions use Image.LANCZOS
_lanczos = getattr(getattr(Image, 'Resampling', None), 'LANCZOS', None) or Image.LANCZOS
img.thumbnail(size, _lanczos)
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=60, optimize=True)
return buf.getvalue()
Expand Down Expand Up @@ -1416,9 +1506,7 @@ def _check_api_key():
if not PANEL_API_KEY:
return # auth devre dışı
if not request.path.startswith('/api/'):
return # HTML paneli korumasız bırak
if request.path == '/health':
return # sağlık kontrolü korumasız
return # HTML paneli ve /health korumasız bırak
key = request.headers.get('X-API-Key')
if not key:
key = request.args.get('api_key')
Expand All @@ -1427,6 +1515,15 @@ def _check_api_key():
if key != PANEL_API_KEY:
return jsonify({"ok": False, "error": "Yetkisiz erişim"}), 401


@app.after_request
def _add_security_headers(response):
"""Her yanıta temel güvenlik başlıkları ekle."""
response.headers.setdefault('X-Content-Type-Options', 'nosniff')
response.headers.setdefault('X-Frame-Options', 'SAMEORIGIN')
response.headers.setdefault('Referrer-Policy', 'strict-origin-when-cross-origin')
return response

# llama-server durumu
_llm_proc = None
_llm_queue: queue.Queue = queue.Queue(maxsize=1000)
Expand Down Expand Up @@ -3088,20 +3185,11 @@ def execute_tool(tool_str):
return {"text": "⚠ Python Sandbox uzmanı deaktif. Ayarlardan aktif edin."}

if name == "calculator":
import math
allowed = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
allowed["abs"] = abs
allowed["round"] = round
allowed["min"] = min
allowed["max"] = max
allowed["sum"] = sum
allowed["int"] = int
allowed["float"] = float
expr_str = t.get("expr", "")
for blocked in ("__", "import", "exec", "eval", "open", "os.", "sys.", "getattr", "setattr"):
if blocked in expr_str:
return f"❌ Güvensiz ifade: '{blocked}' kullanılamaz"
res = eval(expr_str, {"__builtins__": {}}, allowed)
try:
res = _safe_calc_eval(expr_str)
except ValueError as ve:
return {"text": f"❌ Hesaplama hatası: {ve}"}
Comment on lines +3191 to +3192

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The calculator tool only catches ValueError from _safe_calc_eval, but runtime errors like ZeroDivisionError, OverflowError, or TypeError will fall through to the generic execute_tool exception handler and produce an inconsistent/less clear error response. Consider catching Exception here (or in _safe_calc_eval) and converting it into the same user-facing “Hesaplama hatası” shape.

Suggested change
except ValueError as ve:
return {"text": f"❌ Hesaplama hatası: {ve}"}
except Exception as e:
return {"text": f"❌ Hesaplama hatası: {e}"}

Copilot uses AI. Check for mistakes.
return f"✅ Hesap Sonucu: {res}"
elif name == "sandbox":
import werkzeug.utils, subprocess, os, time as _time, base64
Expand Down Expand Up @@ -3152,17 +3240,10 @@ def execute_tool(tool_str):
except Exception as e:
return {"text": f"❌ Araç çalıştırma hatası: {e}"}

# ── Hesap makinesi için safe eval ──────────────────────────
# ── Hesap makinesi için güvenli AST değerlendirici ─────────
def _calc_eval(expr_str: str):
"""Güvenli matematik eval — sadece math fonksiyonları."""
import math
allowed = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
for fn in (abs, round, min, max, sum, int, float):
allowed[fn.__name__] = fn
for blocked in ("__", "import", "exec", "eval", "open", "os.", "sys.", "getattr", "setattr"):
if blocked in expr_str:
raise ValueError(f"Güvensiz ifade: '{blocked}' kullanılamaz")
return eval(expr_str, {"__builtins__": {}}, allowed)
"""Güvenli matematik değerlendirici — AST tabanlı, eval() yok."""
return _safe_calc_eval(expr_str)

# ── Aşama 1: Gizli LLM çağrısı — math JSON extract ──────
def _phase1_extract_math(user_text: str):
Expand Down
9 changes: 9 additions & 0 deletions chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1672,6 +1672,15 @@ def _main(path: str, method: str = 'GET', **kwargs) -> http_req.Response:
timeout=kwargs.pop('timeout', 10), **kwargs)


@app.after_request
def _add_security_headers(response):
"""Her yanıta temel güvenlik başlıkları ekle."""
response.headers.setdefault('X-Content-Type-Options', 'nosniff')
response.headers.setdefault('X-Frame-Options', 'SAMEORIGIN')
response.headers.setdefault('Referrer-Policy', 'strict-origin-when-cross-origin')
return response


@app.route('/')
def index():
"""Chat arayüzünü sun. Cookie yoksa yeni UID ata."""
Expand Down
16 changes: 6 additions & 10 deletions whatsapp_bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,18 +280,14 @@ client.on('message', async msg => {
{ type: 'image_url', image_url: { url: `data:${imgMime};base64,${imgB64}` } }
]
});
// Görseli kaydet (dedup)
try {
await axios.post(`${PANEL}/api/messages/save`,
{ chat_id: personId, role: 'user', content: `[Görsel] ${prompt}` });
} catch { }
// Görseli metin olarak kaydet (LLM çağrısından önce, dedup)
await saveMsg(personId, 'user', `[Görsel] ${prompt}`);
} else {
finalMsgs.push({ role: 'user', content: enhanced });
// Kullanıcı mesajını LLM çağrısından ÖNCE kaydet
await saveMsg(personId, 'user', enhanced);
}

// Kullanıcı mesajını LLM çağrısından ÖNCE kaydet
await saveMsg(personId, 'user', enhanced);

// ── LLM İsteği ──
const res = await axios.post(llamaUrl, {
model: 'local', messages: finalMsgs, temperature: 0.7, stream: false
Expand All @@ -301,12 +297,12 @@ client.on('message', async msg => {

// LaTeX temizliği
reply = reply
.replace(/\$\$(.*?)\$\$/gs, '$1').replace(/\$(.*?)\$/g, '$1')
.replace(/\$\$([\s\S]*?)\$\$/g, '$1').replace(/\$([^$\s](?:[^$]*[^$\s])?)\$/g, '$1')
.replace(/\\text\{([^}]+)\}/g, '$1').replace(/\\boxed\{([^}]+)\}/g, '*$1*')
.replace(/\\frac\{([^}]+)\}\{([^}]+)\}/g, '$1/$2')
.replace(/\\sqrt\{([^}]+)\}/g, '√$1').replace(/\\cdot/g, '·')
.replace(/\\times/g, '×').replace(/\\div/g, '÷')
.replace(/\\implies/g, '=>').replace(/\\[a-zA-Z]+/g, '');
.replace(/\\implies/g, '=>').replace(/\\[a-zA-Z]+\{[^}]*\}/g, '').replace(/\\[a-zA-Z]+/g, '');

// Asistan yanıtını kaydet
await saveMsg(personId, 'assistant', reply);
Expand Down
Loading