mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Add user agent blocklist, use X-Real-IP header for IP detection
This commit is contained in:
@@ -17,6 +17,8 @@ import os
|
||||
|
||||
from security.audit import log_security
|
||||
from security.profanity import contains_profanity
|
||||
from security.user_agent_blocklist import is_user_agent_blocked
|
||||
from security.rate_limit import rate_limit_per_ip, rate_limit_per_user
|
||||
router = APIRouter()
|
||||
|
||||
_FAILED_ATTEMPT_WINDOW_SECONDS = 300
|
||||
@@ -65,9 +67,25 @@ def check_auth(current_user: User = Depends(get_current_user)):
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
@rate_limit_per_ip("5/minute")
|
||||
def login(request: LoginRequest, http: Request, db: Session = Depends(get_db)):
|
||||
username = request.username.strip()
|
||||
client_ip = get_client_ip(http)
|
||||
raw_ua = http.headers.get("user-agent")
|
||||
|
||||
if is_user_agent_blocked(raw_ua):
|
||||
log_security(
|
||||
"blocked_user_agent",
|
||||
severity="warning",
|
||||
username=username,
|
||||
ip=client_ip,
|
||||
user_agent=raw_ua or "Unknown",
|
||||
action="login",
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Доступ запрещён"
|
||||
)
|
||||
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
|
||||
@@ -162,12 +180,28 @@ def login(request: LoginRequest, http: Request, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/register")
|
||||
@rate_limit_per_ip("3/hour")
|
||||
def register(request: RegisterRequest, http: Request, db: Session = Depends(get_db)):
|
||||
username = request.username.strip()
|
||||
display_name = request.display_name.strip()
|
||||
password = request.password.strip()
|
||||
confirm_password = request.confirm_password.strip()
|
||||
client_ip = get_client_ip(http)
|
||||
raw_ua = http.headers.get("user-agent")
|
||||
|
||||
if is_user_agent_blocked(raw_ua):
|
||||
log_security(
|
||||
"blocked_user_agent",
|
||||
severity="warning",
|
||||
username=username,
|
||||
ip=client_ip,
|
||||
user_agent=raw_ua or "Unknown",
|
||||
action="registration",
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Доступ запрещён"
|
||||
)
|
||||
|
||||
# Determine if owner already exists
|
||||
owner_exists = db.query(User).filter(User.username == OWNER_USERNAME).first() is not None
|
||||
@@ -411,6 +445,7 @@ def logout(
|
||||
|
||||
|
||||
@router.post("/change-password")
|
||||
@rate_limit_per_user("5/hour")
|
||||
def change_password(
|
||||
request: ChangePasswordRequest,
|
||||
http: Request,
|
||||
|
||||
@@ -27,6 +27,7 @@ import json
|
||||
from better_profanity import profanity as _bp
|
||||
from security.audit import log_access, log_dm, log_public_chat, log_security
|
||||
from security.profanity import censor_text
|
||||
from security.rate_limit import rate_limit_per_user
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger("uvicorn.error")
|
||||
@@ -251,6 +252,7 @@ def convert_dm_envelope(envelope: DMEnvelope) -> dict:
|
||||
}
|
||||
|
||||
@router.post("/send_message")
|
||||
@rate_limit_per_user("30/minute")
|
||||
async def send_message(
|
||||
request: SendMessageRequest | None = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
@@ -408,6 +410,7 @@ async def get_messages(db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/dm/send")
|
||||
@rate_limit_per_user("20/minute")
|
||||
async def dm_send(
|
||||
payload: dict | None = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
@@ -619,6 +622,7 @@ async def get_dm_conversations(current_user: User = Depends(get_current_user), d
|
||||
|
||||
|
||||
@router.put("/edit_message/{message_id}")
|
||||
@rate_limit_per_user("20/minute")
|
||||
async def edit_message(
|
||||
message_id: int,
|
||||
request: EditMessageRequest,
|
||||
@@ -694,6 +698,7 @@ async def delete_message(
|
||||
|
||||
|
||||
@router.post("/add_reaction")
|
||||
@rate_limit_per_user("50/minute")
|
||||
async def add_reaction(
|
||||
request: ReactionRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
@@ -761,6 +766,7 @@ async def add_reaction(
|
||||
|
||||
|
||||
@router.post("/dm/add_reaction")
|
||||
@rate_limit_per_user("50/minute")
|
||||
async def add_dm_reaction(
|
||||
request: DMReactionRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
|
||||
@@ -7,6 +7,13 @@ from dependencies import get_current_user
|
||||
from models import User
|
||||
from security.audit import log_security
|
||||
from security.profanity import add_to_blocklist, get_blocklist, remove_from_blocklist
|
||||
from security.user_agent_blocklist import (
|
||||
add_to_blocklist as add_ua_to_blocklist,
|
||||
get_blocklist as get_ua_blocklist,
|
||||
get_static_blocklist as get_ua_static_blocklist,
|
||||
get_external_blocklist as get_ua_external_blocklist,
|
||||
remove_from_blocklist as remove_ua_from_blocklist,
|
||||
)
|
||||
|
||||
|
||||
class BlocklistUpdateRequest(BaseModel):
|
||||
@@ -58,3 +65,45 @@ def delete_from_blocklist(
|
||||
)
|
||||
return {"removed": removed, "words": updated}
|
||||
|
||||
|
||||
@router.get("/user-agent-blocklist")
|
||||
def list_user_agent_blocklist(current_user: User = Depends(get_current_user)):
|
||||
_ensure_owner(current_user)
|
||||
return {
|
||||
"patterns": get_ua_blocklist(),
|
||||
"static": get_ua_static_blocklist(),
|
||||
"external": get_ua_external_blocklist(),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/user-agent-blocklist")
|
||||
def append_user_agent_blocklist(
|
||||
request: BlocklistUpdateRequest,
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
_ensure_owner(current_user)
|
||||
added, updated = add_ua_to_blocklist(request.words)
|
||||
log_security(
|
||||
"user_agent_blocklist_add",
|
||||
actor=current_user.username,
|
||||
actor_id=current_user.id,
|
||||
added=added,
|
||||
)
|
||||
return {"added": added, "patterns": updated}
|
||||
|
||||
|
||||
@router.delete("/user-agent-blocklist")
|
||||
def delete_from_user_agent_blocklist(
|
||||
request: BlocklistUpdateRequest,
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
_ensure_owner(current_user)
|
||||
removed, updated = remove_ua_from_blocklist(request.words)
|
||||
log_security(
|
||||
"user_agent_blocklist_remove",
|
||||
actor=current_user.username,
|
||||
actor_id=current_user.id,
|
||||
removed=removed,
|
||||
)
|
||||
return {"removed": removed, "patterns": updated}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from similarity import is_user_similar_to_verified
|
||||
from .messaging import messagingManager
|
||||
from security.audit import log_security
|
||||
from security.profanity import contains_profanity
|
||||
from security.rate_limit import rate_limit_per_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -39,6 +40,7 @@ PROFILE_PICTURES_DIR = Path("data/uploads/pfp")
|
||||
os.makedirs(PROFILE_PICTURES_DIR, exist_ok=True)
|
||||
|
||||
@router.post("/upload-profile-picture")
|
||||
@rate_limit_per_user("10/minute")
|
||||
async def upload_profile_picture(
|
||||
profile_picture: UploadFile = File(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
@@ -163,6 +165,7 @@ async def list_users(
|
||||
}
|
||||
|
||||
@router.put("/user/profile")
|
||||
@rate_limit_per_user("10/minute")
|
||||
async def update_user_profile(
|
||||
request: UpdateProfileRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
@@ -239,6 +242,7 @@ async def update_user_profile(
|
||||
|
||||
|
||||
@router.put("/user/bio")
|
||||
@rate_limit_per_user("10/minute")
|
||||
async def update_user_bio(
|
||||
request: UpdateBioRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
|
||||
Reference in New Issue
Block a user