FIX: ip detction and ip hashing

This commit is contained in:
Your Name 2026-07-22 16:13:41 +03:00
parent b5982063c4
commit 80da311288
8 changed files with 68 additions and 18 deletions

BIN
chat.db

Binary file not shown.

View File

@ -37,6 +37,23 @@ def init(db: DBfrontend, sec_cfg):
_salt = sec_cfg.salt
def _get_client_ip() -> str:
forwarded = request.get_header("X-Forwarded-For")
if forwarded:
return forwarded.split(",")[0].strip()
real_ip = request.get_header("X-Real-IP")
if real_ip:
return real_ip.strip()
client_id = request.get_header("X-Client-Id")
if client_id:
return "client-" + client_id
return request.environ.get("REMOTE_ADDR", "127.0.0.1")
def _hash_ip(ip: str) -> str:
return hashlib.sha256((_salt + ip).encode()).hexdigest()[:16]
def _check_rate(ip: str) -> bool:
now = time.time()
cutoff = now - _rate_window
@ -70,7 +87,7 @@ def register_routes(app):
@app.route("/api/messages", method="POST")
def post_message():
ip = request.environ.get("REMOTE_ADDR", "127.0.0.1")
ip = _get_client_ip()
if not _check_rate(ip):
response.status = 429
return {"success": False, "error": "Rate limit exceeded. Slow down."}
@ -97,7 +114,7 @@ def register_routes(app):
content = _strip_xss(content)
ip_hash = hashlib.sha256((_salt + ip).encode()).hexdigest()[:16]
ip_hash = _hash_ip(ip)
msg_id = _db.add_message(channel, ip_hash, content, gif_url=gif_url)
return {"success": True, "message_id": msg_id, "ip": ip_hash}
@ -134,7 +151,7 @@ def register_routes(app):
@app.route("/api/polls", method="POST")
def create_poll():
ip = request.environ.get("REMOTE_ADDR", "127.0.0.1")
ip = _get_client_ip()
if not _check_rate(ip):
response.status = 429
return {"success": False, "error": "Rate limit exceeded. Slow down."}
@ -172,7 +189,7 @@ def register_routes(app):
response.status = 400
return {"success": False, "error": "Option too long"}
ip_hash = hashlib.sha256((_salt + ip).encode()).hexdigest()[:16]
ip_hash = _hash_ip(ip)
expires_at = data.get("expires_at")
is_multiple = data.get("is_multiple", False)
is_anonymous = data.get("is_anonymous", False)
@ -217,14 +234,14 @@ def register_routes(app):
@app.route("/api/polls/<poll_id:int>/vote", method="POST")
def vote_poll(poll_id):
ip = request.environ.get("REMOTE_ADDR", "127.0.0.1")
ip = _get_client_ip()
try:
data = request.json
if not data or "option_id" not in data:
response.status = 400
return {"success": False, "error": "option_id required"}
ip_hash = hashlib.sha256((_salt + ip).encode()).hexdigest()[:16]
ip_hash = _hash_ip(ip)
ok, msg = _db.vote(poll_id, data["option_id"], ip_hash)
if not ok:
response.status = 400
@ -236,8 +253,8 @@ def register_routes(app):
@app.route("/api/polls/<poll_id:int>/close", method="POST")
def close_poll(poll_id):
ip = request.environ.get("REMOTE_ADDR", "127.0.0.1")
ip_hash = hashlib.sha256((_salt + ip).encode()).hexdigest()[:16]
ip = _get_client_ip()
ip_hash = _hash_ip(ip)
auth = request.get_header("Authorization", "")
token = auth.replace("Bearer ", "")

View File

@ -524,6 +524,21 @@
let selectedGifUrl = null;
let votedPolls = new Set();
function getClientId() {
let id = localStorage.getItem('clientId');
if (!id) {
id = crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2) + Date.now().toString(36);
localStorage.setItem('clientId', id);
}
return id;
}
function apiFetch(url, options = {}) {
const headers = options.headers || {};
headers['X-Client-Id'] = getClientId();
return fetch(url, { ...options, headers });
}
function loadVotedPolls() {
try {
const saved = localStorage.getItem('votedPolls_' + currentChannel);
@ -560,7 +575,7 @@
const url = query
? `/api/gifs/search?q=${encodeURIComponent(query)}&limit=20`
: `/api/gifs/trending?limit=20`;
const r = await fetch(url);
const r = await apiFetch(url);
const data = await r.json();
if (data.success) renderGifs(data.results || []);
else gifGrid.innerHTML = '<div class="empty-state">Failed to load GIFs</div>';
@ -695,7 +710,7 @@
pollCreate.disabled = true;
try {
const r = await fetch('/api/polls', {
const r = await apiFetch('/api/polls', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@ -748,7 +763,7 @@
async function loadMessages() {
try {
const r = await fetch(`/api/messages?channel=${currentChannel}&limit=50`);
const r = await apiFetch(`/api/messages?channel=${currentChannel}&limit=50`);
const data = await r.json();
if (data.success) {
window._lastMessages = data.messages || [];
@ -830,7 +845,7 @@
if (votedPolls.has(pollId)) return;
el.addEventListener('click', async () => {
try {
const r = await fetch(`/api/polls/${pollId}/vote`, {
const r = await apiFetch(`/api/polls/${pollId}/vote`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ option_id: optId })
@ -861,7 +876,7 @@
sendBtn.disabled = true;
try {
const r = await fetch('/api/messages', {
const r = await apiFetch('/api/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel: currentChannel, message: payload, gif_url: gif_url })

View File

@ -61,8 +61,8 @@ class PollOption(Base):
class PollVote(Base):
__tablename__ = "poll_votes"
__table_args__ = (
UniqueConstraint("poll_id", "voter_ip",
name="uq_poll_voter"),
UniqueConstraint("poll_id", "option_id", "voter_ip",
name="uq_poll_voter_option"),
)
id = Column(Integer, primary_key=True)

View File

@ -9,8 +9,9 @@ from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import IntegrityError
from .base import make_engine, Messages, Poll, PollOption, PollVote
from .base import make_engine, Base, Messages, Poll, PollOption, PollVote
class DBfrontend:
@ -24,10 +25,23 @@ class DBfrontend:
with self.Session() as session:
from sqlalchemy import inspect, text
inspector = inspect(session.connection())
columns = [c["name"] for c in inspector.get_columns("messages")]
if "poll_id" not in columns:
session.execute(text("ALTER TABLE messages ADD COLUMN poll_id INTEGER"))
session.commit()
constraints = [uq["name"] for uq in inspector.get_unique_constraints("poll_votes")]
if "uq_poll_voter" in constraints:
old = "poll_votes_old"
session.execute(text(f"ALTER TABLE poll_votes RENAME TO {old}"))
PollVote.__table__.create(session.connection())
session.execute(text(f"""
INSERT INTO poll_votes (id, poll_id, option_id, voter_ip, voted_at)
SELECT id, poll_id, option_id, voter_ip, voted_at FROM {old}
"""))
session.execute(text(f"DROP TABLE {old}"))
session.commit()
@staticmethod
def _get_cipher(secret: bytes, salt: bytes) -> Fernet:
@ -228,7 +242,11 @@ class DBfrontend:
voted_at=int(time.time() * 1000),
)
session.add(vote)
session.commit()
try:
session.commit()
except IntegrityError:
session.rollback()
return False, "You have already voted in this poll"
return True, "Vote recorded"
def close_poll(self, poll_id: int, requester_ip: str) -> tuple[bool, str]: