From 7e0673d018696c70bc0c471e344cb7e0c879a673 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaakko=20Kera=CC=88nen?= jaakko.keranen@iki.fi
Date: Fri, 27 Oct 2023 11:09:57 +0300
Subject: [PATCH 1/1] Rate limiting of registrations and posts by limited users
Also fixed token verification when posting.
50_bubble.py | 5 +++++
feeds.py | 31 +++++++++++++++++++++----------
model.py | 43 +++++++++++++++++++++++++++++++++++++++++++
3 files changed, 69 insertions(+), 10 deletions(-)
diff --git a/50_bubble.py b/50_bubble.py
index 961f2e6..4eff66b 100644
--- a/50_bubble.py
+++ b/50_bubble.py
@@ -38,6 +38,8 @@ class Bubble:
self.user_register = cfg.getboolean('user.register', True)
self.user_subspaces = cfg.getboolean('user.subspaces', True)
self.user_role_limited = cfg.getboolean('user.role.limited', True)
self.rate_register = max(1, cfg.getint('rate.register', 20)) # per hour, any remote
self.rate_post = max(1, cfg.getint('rate.post', 10)) # per hour, per remote
self.admin_certpass = cfg.get('admin.certpass', '')
self.antenna_url = cfg.get('antenna.url', 'gemini://warmedal.se/~antenna/submit')
self.version = __version__
@@ -448,6 +450,8 @@ Bubble is open source:
if req.path.startswith(self.path + 'register/'):
if not session.bubble.user_register:
return 61, 'User registration is closed'
if db.get_access_rate(3600, None, LogEntry.ACCOUNT_CREATED) >= session.bubble.rate_register:
return 44, str(int(3600 / session.bubble.rate_register))
if not db.verify_token(session.user, req.path[len(self.path + 'register/'):]):
return 61, 'Expired'
try:
@@ -458,6 +462,7 @@ Bubble is open source:
db.create_user(username, req.identity,
User.LIMITED if session.bubble.user_role_limited else \
User.BASIC)
db.add_log_entry(req.remote_address, LogEntry.ACCOUNT_CREATED)
page = f'# Welcome, {username}!\n\n'
if session.bubble.user_role_limited:
diff --git a/feeds.py b/feeds.py
index 06e92c3..983a164 100644
--- a/feeds.py
+++ b/feeds.py
@@ -1,6 +1,6 @@
import re
import urllib.parse as urlparse
-from model import User, Post, Segment, Subspace, Commit, Crossref, \
+from model import User, Post, Segment, Subspace, LogEntry, Commit, Crossref, \
FOLLOW_SUBSPACE, FOLLOW_USER, FOLLOW_POST, \
MUTE_SUBSPACE, MUTE_USER, MUTE_POST
from subspace import subspace_admin_actions
@@ -99,11 +99,15 @@ def make_post_page_or_configure_feed(session):
if session.is_context_locked:
return 61, "Subspace is locked"
if session.user.role == User.LIMITED and (not session.c_user or
not session.c_user.id != session.user.id):
session.c_user.id != session.user.id):
return 61, "Not authorized"
if session.user.role == User.LIMITED and not db.verify_token(session.user, arg2):
if session.user.role == User.LIMITED and not db.verify_token(session.user, arg):
return 61, "Expired"
if session.user.role == User.LIMITED and \
db.get_access_rate(3600, req.remote_address, LogEntry.POST_CREATED) >= session.bubble.rate_post:
return 44, str(int(3600 / session.bubble.rate_post))
draft_id = db.create_post(session.user, session.context.id)
db.add_log_entry(req.remote_address, LogEntry.POST_CREATED)
return 30, '/edit/%d' % draft_id
elif action == 'post':
@@ -115,8 +119,11 @@ def make_post_page_or_configure_feed(session):
return 61, "Subspace is locked"
if session.user.role == User.LIMITED and not session.c_user:
return 61, "Not authorized"
if session.user.role == User.LIMITED and not db.verify_token(session.user, arg2):
if session.user.role == User.LIMITED and not db.verify_token(session.user, arg):
return 61, "Expired"
if session.user.role == User.LIMITED and \
db.get_access_rate(3600, req.remote_address, LogEntry.POST_CREATED) >= session.bubble.rate_post:
return 44, str(int(3600 / session.bubble.rate_post))
if session.is_gemini:
if is_empty_query(req):
@@ -181,6 +188,7 @@ def make_post_page_or_configure_feed(session):
return 30, f'{session.server_root()}/edit/{post.id}'
db.publish_post(post)
db.add_log_entry(req.remote_address, LogEntry.POST_CREATED)
return 30, session.server_root() + post.page_url()
if session.user:
@@ -629,12 +637,15 @@ def make_feed_page(session):
page += f'=> /s/ {session.bubble.site_icon} Subspaces\n'
page += session.FOOTER_MENU
else:
token = session.get_token()
if session.user.role == User.LIMITED:
link_suffix = '/' + session.get_token()
else:
link_suffix = ''
page += session.dashboard_link()
if not session.is_context_locked:
if c_user and c_user.id == user.id:
page += f'=> /u/{user.name}/post/{token} 💬 New post\n'
page += f'=> /u/{user.name}/compose/{token} ✏️ Compose draft\n'
page += f'=> /u/{user.name}/post{link_suffix} 💬 New post\n'
page += f'=> /u/{user.name}/compose{link_suffix} ✏️ Compose draft\n'
elif context and context.owner == 0:
if is_issue_tracker:
if session.user.role != User.LIMITED:
@@ -642,10 +653,10 @@ def make_feed_page(session):
else:
if session.user.role != User.LIMITED:
page += f'=> /{context.title()}/post 💬 New post in s/{context.name}\n'
page += f'=> /{context.title()}/compose/{token} ✏️ Compose draft in s/{context.name}\n'
page += f'=> /{context.title()}/compose{link_suffix} ✏️ Compose draft in s/{context.name}\n'
else:
page += f'=> /u/{user.name}/post/{token} 💬 New post in u/{user.name}\n'
page += f'=> /u/{user.name}/compose/{token} ✏️ Compose draft in u/{user.name}\n'
page += f'=> /u/{user.name}/post{link_suffix} 💬 New post in u/{user.name}\n'
page += f'=> /u/{user.name}/compose{link_suffix} ✏️ Compose draft in u/{user.name}\n'
page += f'=> /s/ {session.bubble.site_icon} Subspaces\n'
if is_issue_tracker:
diff --git a/model.py b/model.py
index e1e9a40..ca39e44 100644
--- a/model.py
+++ b/model.py
@@ -1,4 +1,5 @@
import datetime
+import hashlib
import mariadb
import os
import random
@@ -25,11 +26,21 @@ def parse_asn1_time(asn1_bytes):
return None
+def address_hash(from_addr):
FOLLOW_USER, FOLLOW_POST, FOLLOW_SUBSPACE = range(3)
MUTE_USER, MUTE_POST, MUTE_SUBSPACE = range(3)
+class LogEntry:
class Segment:
TEXT, LINK, IMAGE, ATTACHMENT, POLL = range(5)
@@ -637,6 +648,14 @@ class Database:
INDEX (dst_issueid)
)""")
db.execute("""CREATE TABLE IF NOT EXISTS log (
remote CHAR(64) NOT NULL,
type INT NOT NULL,
ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX (remote),
INDEX (type)
)""")
db.execute('INSERT IGNORE INTO users (name, avatar, role, password, ts_password) '
'VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP())',
('admin', '🚀', User.ADMIN, admin_certpass))
@@ -2402,6 +2421,30 @@ class Database:
commits.append(Commit(repo.id, hash, msg, ts))
return commits
cond = ['type=?']
values = [entry_type]
if from_addr != None:
cond.append('remote=?')
values.append(address_hash(from_addr))
cond.append('TIMESTAMPDIFF(SECOND, ts, CURRENT_TIMESTAMP())<?')
values.append(window_seconds)
cur = self.conn.cursor()
cur.execute(f"SELECT COUNT(*) FROM log WHERE {' AND '.join(cond)}", values)
for (rate,) in cur:
pass
cur.execute("DELETE FROM log WHERE TIMESTAMPDIFF(MINUTE, ts, CURRENT_TIMESTAMP())>=60")
self.commit()
return rate
cur = self.conn.cursor()
cur.execute("INSERT INTO log (remote, type) VALUES (?, ?)", (address_hash(from_addr), type))
self.commit()
class Search:
def __init__(self, db):
--
2.25.1
text/plain
This content has been proxied by September (3851b).