add web
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import time
|
||||
import string
|
||||
import secrets
|
||||
import pyotp
|
||||
|
||||
from argon2 import PasswordHasher
|
||||
from config import Config
|
||||
from flask import request, redirect, jsonify, make_response, abort, g
|
||||
from functools import wraps
|
||||
|
||||
from app.models.user import User
|
||||
from app.models.placeservers import PlaceServer
|
||||
from app.models.gameservers import GameServer
|
||||
from app.extensions import db, redis_controller, get_remote_address
|
||||
|
||||
config = Config()
|
||||
|
||||
def ValidateToken( token : str ) -> bool:
|
||||
TokenInfo = redis_controller.get("authtoken_" + token)
|
||||
if TokenInfo is None:
|
||||
return False
|
||||
|
||||
# Token Info Format
|
||||
# userid|created|expiry|ip
|
||||
TokenInfo = TokenInfo.split("|")
|
||||
if len(TokenInfo) != 4:
|
||||
redis_controller.delete("authtoken_" + token)
|
||||
return False
|
||||
|
||||
if int(TokenInfo[2]) < int(time.time()):
|
||||
redis_controller.delete("authtoken_" + token)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def GetAuthenticatedUser( token : str ) -> User:
|
||||
AuthTokenInfo = GetTokenInfo(token)
|
||||
if AuthTokenInfo is None:
|
||||
return None
|
||||
return User.query.filter_by(id=int(AuthTokenInfo[0])).first()
|
||||
|
||||
def GetTokenInfo( token : str ) -> list:
|
||||
TokenInfo = redis_controller.get("authtoken_" + token)
|
||||
if TokenInfo is None:
|
||||
return None
|
||||
|
||||
# Token Info Format
|
||||
# userid|created|expiry|ip
|
||||
TokenInfo = TokenInfo.split("|")
|
||||
if len(TokenInfo) != 4:
|
||||
redis_controller.delete("authtoken_" + token)
|
||||
return None
|
||||
|
||||
if int(TokenInfo[2]) < int(time.time()):
|
||||
redis_controller.delete("authtoken_" + token)
|
||||
return None
|
||||
|
||||
return TokenInfo
|
||||
|
||||
def CreateToken( userid : int , ip, expireIn : int = (60 * 60 * 24 * 31)) -> str:
|
||||
Token = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(512))
|
||||
|
||||
# Token Info Format
|
||||
# userid|created|expiry|ip
|
||||
|
||||
TokenInfo = str(userid) + "|" + str(int(time.time())) + "|" + str(int(time.time()) + expireIn) + "|" + ip
|
||||
redis_controller.set("authtoken_" + Token, TokenInfo, ex = expireIn)
|
||||
|
||||
return Token
|
||||
|
||||
def isAuthenticated():
|
||||
if ".ROBLOSECURITY" not in request.cookies:
|
||||
return False
|
||||
if not ValidateToken(request.cookies[".ROBLOSECURITY"]):
|
||||
return False
|
||||
return True
|
||||
|
||||
def authenticated_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if ".ROBLOSECURITY" not in request.cookies:
|
||||
return redirect("/login")
|
||||
if not ValidateToken(request.cookies[".ROBLOSECURITY"]):
|
||||
RedirectResponse = make_response(redirect("/login"))
|
||||
RedirectResponse.set_cookie(".ROBLOSECURITY", expires=0)
|
||||
return RedirectResponse
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
def authenticated_required_api(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if ".ROBLOSECURITY" not in request.cookies:
|
||||
return jsonify({"success": False, "message": "You are not logged in"}), 401
|
||||
if not ValidateToken(request.cookies[".ROBLOSECURITY"]):
|
||||
ErrorResponse = make_response(jsonify({"success": False, "message": "You are not logged in"}), 401)
|
||||
ErrorResponse.set_cookie(".ROBLOSECURITY", expires=0)
|
||||
return ErrorResponse
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
def authenticated_client_endpoint(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if request.cookies.get("Syntax-Session-Id", type = str ) is not None:
|
||||
DataSections = request.cookies.get("Syntax-Session-Id", type = str )
|
||||
else:
|
||||
return jsonify({"success": False, 'error': 'Missing Headers'}), 401
|
||||
|
||||
if len(DataSections) != 9:
|
||||
return jsonify({"success": False, 'error': 'Invalid Headers'}), 401
|
||||
|
||||
AuthToken = DataSections[8]
|
||||
if not ValidateToken(AuthToken):
|
||||
return jsonify({"success": False, 'error': 'Invalid Token'}), 401
|
||||
|
||||
PlaceServerObj : PlaceServer = PlaceServer.query.filter_by(serveruuid=str(DataSections[1])).first()
|
||||
if PlaceServerObj is None:
|
||||
invalidateToken(AuthToken)
|
||||
return jsonify({"success": False, 'error': 'Invalid Token'}), 401
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
def gameserver_authenticated_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if "Roblox/" not in request.user_agent.string:
|
||||
abort(404)
|
||||
RemoteAddress = get_remote_address()
|
||||
GameServerObj : GameServer = GameServer.query.filter_by( serverIP = RemoteAddress ).first()
|
||||
if GameServerObj is None:
|
||||
abort(404)
|
||||
requesterAccessKey = request.headers.get( key = "AccessKey", type = str, default = "")
|
||||
if "UserRequest" in requesterAccessKey:
|
||||
logging.warning(f"GameServer {RemoteAddress} - UserRequest access key used for {request.url}")
|
||||
abort(404)
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
def gameserver_accesskey_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if "AccessKey" not in request.headers:
|
||||
abort(404)
|
||||
RequesterAccessKey = request.headers.get( key = "AccessKey", type = str, default = "")
|
||||
RemoteAddress = get_remote_address()
|
||||
GameServerObj : GameServer = GameServer.query.filter_by( serverIP = RemoteAddress, accessKey = RequesterAccessKey ).first()
|
||||
if GameServerObj is None:
|
||||
abort(404)
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
def invalidateToken(token : str):
|
||||
if token is not None:
|
||||
redis_controller.delete("authtoken_" + token)
|
||||
|
||||
def Validate2FACode( userid : int, code : str ) -> bool:
|
||||
from app.pages.settings.settings import generate_secret_key_from_string
|
||||
"""
|
||||
Validates a 2FA code for a user
|
||||
|
||||
:param userid: The user id to validate the code for
|
||||
:param code: The code to validate
|
||||
|
||||
:returns: bool (True if correct, False if not)
|
||||
"""
|
||||
user : User = User.query.filter_by(id=userid).first()
|
||||
if user is None:
|
||||
return False
|
||||
if user.TOTPEnabled == False:
|
||||
return False
|
||||
totp = pyotp.TOTP(generate_secret_key_from_string(str(user.id) + config.FLASK_SESSION_KEY))
|
||||
return totp.verify(code)
|
||||
|
||||
def GetCurrentUser() -> User:
|
||||
"""
|
||||
Gets the current user from the request
|
||||
|
||||
:returns: User (User object if logged in, None if not)
|
||||
"""
|
||||
|
||||
def _get_user() -> User:
|
||||
if ".ROBLOSECURITY" not in request.cookies:
|
||||
if "Syntax-Session-Id" not in request.cookies:
|
||||
return None
|
||||
else:
|
||||
DataSections = request.cookies["Syntax-Session-Id"].split("|")
|
||||
if len(DataSections) != 9:
|
||||
return None
|
||||
|
||||
AuthToken = DataSections[8]
|
||||
if not ValidateToken(AuthToken):
|
||||
return None
|
||||
|
||||
PlaceServerObj : PlaceServer = PlaceServer.query.filter_by(serveruuid=str(DataSections[1])).first()
|
||||
if PlaceServerObj is None:
|
||||
invalidateToken(AuthToken)
|
||||
return None
|
||||
|
||||
AuthTokenInfo = GetTokenInfo(AuthToken)
|
||||
return User.query.filter_by(id=int(AuthTokenInfo[0])).first()
|
||||
if not ValidateToken(request.cookies[".ROBLOSECURITY"]):
|
||||
return None
|
||||
AuthTokenInfo = GetTokenInfo(request.cookies[".ROBLOSECURITY"])
|
||||
if AuthTokenInfo is None:
|
||||
return None
|
||||
return User.query.filter_by(id=int(AuthTokenInfo[0])).first()
|
||||
|
||||
if not hasattr(g, 'current_authenticated_user'):
|
||||
g.current_authenticated_user = _get_user()
|
||||
return g.current_authenticated_user
|
||||
|
||||
|
||||
def _GetArgonSalt( UserObj : User ) -> bytes:
|
||||
return (config.FLASK_SESSION_KEY + str(UserObj.id)).encode('utf-8')
|
||||
def _GetPasswordHasher() -> PasswordHasher:
|
||||
return PasswordHasher(
|
||||
time_cost=16,
|
||||
memory_cost=2**14,
|
||||
parallelism=2,
|
||||
hash_len=32,
|
||||
salt_len=16
|
||||
)
|
||||
|
||||
def VerifyPassword( UserObj : User, password : str ) -> bool:
|
||||
"""
|
||||
Verifies the password of the given user
|
||||
|
||||
:param UserObj: The user object to verify the password of
|
||||
:param password: The password to verify
|
||||
|
||||
:returns: bool (True if correct, False if not)
|
||||
"""
|
||||
if config.SWITCH_TO_ARGON_PASSWORD_HASH: # Switch this when we migrate to argon
|
||||
argon_ph = _GetPasswordHasher()
|
||||
if not UserObj.password.startswith("$argon2id"): # Handle old passwords in sha512
|
||||
isCorrect = hashlib.sha512(password.encode('utf-8')).hexdigest() == UserObj.password
|
||||
if isCorrect:
|
||||
UserObj.password = PasswordHasher().hash(password, salt = _GetArgonSalt(UserObj) )
|
||||
logging.info(f"User {UserObj.username} [{UserObj.id}] migrated to argon2id password hash")
|
||||
db.session.commit()
|
||||
return isCorrect
|
||||
|
||||
try:
|
||||
return argon_ph.verify(UserObj.password, password)
|
||||
except:
|
||||
return False
|
||||
else:
|
||||
return hashlib.sha512(password.encode('utf-8')).hexdigest() == UserObj.password
|
||||
|
||||
def SetPassword( UserObj : User, password : str ):
|
||||
"""
|
||||
Sets the password of the given user
|
||||
|
||||
:param UserObj: The user object to set the password of
|
||||
:param password: The password to set
|
||||
|
||||
:returns: bool (True if successful, False if not)
|
||||
"""
|
||||
if config.SWITCH_TO_ARGON_PASSWORD_HASH: # Switch this when we migrate to argon
|
||||
argon_ph = _GetPasswordHasher()
|
||||
UserObj.password = argon_ph.hash(password, salt = _GetArgonSalt(UserObj) )
|
||||
else:
|
||||
UserObj.password = hashlib.sha512(password.encode('utf-8')).hexdigest()
|
||||
|
||||
db.session.commit()
|
||||
return True
|
||||
Reference in New Issue
Block a user