FIX: ip detction and ip hashing
This commit is contained in:
parent
b5982063c4
commit
80da311288
Binary file not shown.
|
|
@ -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 ", "")
|
||||
|
|
|
|||
25
index.html
25
index.html
|
|
@ -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 })
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -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)
|
||||
|
|
|
|||
24
models/db.py
24
models/db.py
|
|
@ -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]:
|
||||
|
|
|
|||
Loading…
Reference in New Issue