add web
This commit is contained in:
@@ -0,0 +1,725 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response, jsonify, after_this_request, Response
|
||||
from app.util import auth, websiteFeatures
|
||||
from app.extensions import db, redis_controller, csrf, get_remote_address
|
||||
import uuid
|
||||
import requests
|
||||
import time
|
||||
from config import Config
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
import secrets
|
||||
import random
|
||||
import string
|
||||
import logging
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
from app.models.user import User
|
||||
from app.models.gameservers import GameServer
|
||||
from app.models.placeservers import PlaceServer
|
||||
from app.models.placeserver_players import PlaceServerPlayer
|
||||
from app.models.place import Place
|
||||
from app.models.asset import Asset
|
||||
from app.models.login_records import LoginRecord
|
||||
from app.models.user_hwid_log import UserHWIDLog
|
||||
from app.models.universe import Universe
|
||||
from app.models.asset_version import AssetVersion
|
||||
from app.enums.AssetType import AssetType
|
||||
from app.enums.MembershipType import MembershipType
|
||||
from app.enums.PlaceYear import PlaceYear
|
||||
from app.services.gameserver_comm import perform_post
|
||||
from app.util.membership import GetUserMembership
|
||||
from app.util.signscript import signUTF8
|
||||
from app.util.assetversion import GetLatestAssetVersion
|
||||
from app.routes.jobreporthandler import EvictPlayer
|
||||
from app.routes.asset import GenerateTempAuthToken
|
||||
|
||||
config = Config()
|
||||
|
||||
class PlaceServerCooldownStart( Exception ):
|
||||
pass
|
||||
class NoAvailableGameServers( Exception ):
|
||||
pass
|
||||
class MissingData( Exception ):
|
||||
pass
|
||||
class UnsupportedPlaceYear( Exception ):
|
||||
pass
|
||||
class UnexpectedStatusCode( Exception ):
|
||||
pass
|
||||
class BadResponseData( Exception ):
|
||||
pass
|
||||
def CreateNewPlaceServer( placeId : int, reserved_server_access_code : str = None ) -> PlaceServer:
|
||||
"""
|
||||
Starts a new PlaceServer for the given placeId, raises appropriate exceptions if it fails
|
||||
|
||||
:param placeId: The placeId to start a new PlaceServer for
|
||||
:reserved_server_access_code: The reserved server access code to use, if None it will be a public server
|
||||
|
||||
:return: PlaceServer object if successful
|
||||
"""
|
||||
CooldownKeyName : str = f"create_new_place_server:{placeId}:{reserved_server_access_code}"
|
||||
if redis_controller.get(CooldownKeyName) is not None:
|
||||
logging.debug(f"CreateNewPlaceServer -> Place {placeId} recently requested to create a new place server, skipping")
|
||||
raise PlaceServerCooldownStart("")
|
||||
redis_controller.setex(CooldownKeyName, 40, "1")
|
||||
|
||||
SelectedGameServerObj : GameServer = GameServer.query.filter_by(
|
||||
allowGameServerHost = True
|
||||
).filter(
|
||||
GameServer.lastHeartbeat > datetime.utcnow() - timedelta(seconds=30)
|
||||
).order_by(
|
||||
GameServer.RCCmemoryUsage.asc()
|
||||
).first()
|
||||
|
||||
if SelectedGameServerObj is None:
|
||||
logging.error(f"CreateNewPlaceServer -> Failed to find a available Gameserver to host Place {placeId}")
|
||||
raise NoAvailableGameServers("")
|
||||
|
||||
PlaceObj : Place = Place.query.filter_by( placeid = placeId ).first()
|
||||
AssetObj : Asset = Asset.query.filter_by( id = placeId ).first()
|
||||
|
||||
if PlaceObj is None or AssetObj is None:
|
||||
logging.error(f"CreateNewPlaceServer -> Failed to find Place / Asset {placeId} in database")
|
||||
raise MissingData("")
|
||||
|
||||
UniverseObj : Universe = Universe.query.filter_by( id = PlaceObj.parent_universe_id ).first()
|
||||
if UniverseObj is None:
|
||||
logging.error(f"CreateNewPlaceServer -> Failed to find Universe {PlaceObj.parent_universe_id} in database for Place {placeId}")
|
||||
raise MissingData("")
|
||||
|
||||
logging.info(f"CreateNewPlaceServer -> Attempting to start new place server for Place {PlaceObj.placeid}, Universe {UniverseObj.id} on Gameserver {SelectedGameServerObj.serverName} ({SelectedGameServerObj.serverId}), reserved_server_access_code: {reserved_server_access_code}")
|
||||
|
||||
RequestSession = requests.Session()
|
||||
RequestSession.headers.update({
|
||||
"Authorization": SelectedGameServerObj.accessKey
|
||||
})
|
||||
|
||||
JSONOpenPayload = {}
|
||||
GameOpenRoute = "Game"
|
||||
JobId : str = str( uuid.uuid4() )
|
||||
CommApiKey : str = str( uuid.uuid4() )
|
||||
|
||||
LatestPlaceVersion : AssetVersion = GetLatestAssetVersion( AssetObj )
|
||||
|
||||
if UniverseObj.place_year == PlaceYear.Sixteen:
|
||||
TempPlaceAuthorizationToken : str = GenerateTempAuthToken( placeId, Expiration = 200, CreatorIP = None )
|
||||
JSONOpenPayload = {
|
||||
"placeid": placeId,
|
||||
"creatorId": AssetObj.creator_id,
|
||||
"creatorType": AssetObj.creator_type,
|
||||
"SpecialAccessToken": TempPlaceAuthorizationToken,
|
||||
"useNewLoadFile": False,
|
||||
"loadfile_location": f"{config.BaseURL}/game/gameserver2016.lua",
|
||||
"universeid": UniverseObj.id,
|
||||
"place_version": LatestPlaceVersion.version
|
||||
}
|
||||
elif UniverseObj.place_year == PlaceYear.Eighteen:
|
||||
GameOpenRoute = "Game2018"
|
||||
JSONOpenPayload = {
|
||||
"placeid": placeId,
|
||||
"creatorId": AssetObj.creator_id,
|
||||
"creatorType": "User" if AssetObj.creator_type == 0 else "Group",
|
||||
"jobid": JobId,
|
||||
"apikey": CommApiKey,
|
||||
"maxplayers": PlaceObj.maxplayers,
|
||||
"address": SelectedGameServerObj.serverIP,
|
||||
"universeid": UniverseObj.id,
|
||||
"place_version": LatestPlaceVersion.version
|
||||
}
|
||||
elif UniverseObj.place_year == PlaceYear.Twenty:
|
||||
GameOpenRoute = "Game2020"
|
||||
JSONOpenPayload = {
|
||||
"placeid": placeId,
|
||||
"creatorId": AssetObj.creator_id,
|
||||
"creatorType": "User" if AssetObj.creator_type == 0 else "Group",
|
||||
"jobid": JobId,
|
||||
"apikey": CommApiKey,
|
||||
"maxplayers": PlaceObj.maxplayers,
|
||||
"address": SelectedGameServerObj.serverIP,
|
||||
"universeid": UniverseObj.id,
|
||||
"place_version": LatestPlaceVersion.version
|
||||
}
|
||||
elif UniverseObj.place_year == PlaceYear.Fourteen:
|
||||
GameOpenRoute = "Game2014"
|
||||
JSONOpenPayload = {
|
||||
"placeid": placeId,
|
||||
"creatorId": AssetObj.creator_id,
|
||||
"creatorType": AssetObj.creator_type,
|
||||
"universeid": UniverseObj.id,
|
||||
"place_version": LatestPlaceVersion.version
|
||||
}
|
||||
elif UniverseObj.place_year == PlaceYear.TwentyOne:
|
||||
GameOpenRoute = "Game2021"
|
||||
JSONOpenPayload = {
|
||||
"placeid": placeId,
|
||||
"creatorId": AssetObj.creator_id,
|
||||
"creatorType": "User" if AssetObj.creator_type == 0 else "Group",
|
||||
"jobid": JobId,
|
||||
"apikey": CommApiKey,
|
||||
"maxplayers": PlaceObj.maxplayers,
|
||||
"address": SelectedGameServerObj.serverIP,
|
||||
"universeid": UniverseObj.id,
|
||||
"place_version": LatestPlaceVersion.version
|
||||
}
|
||||
else:
|
||||
logging.error(f"CreateNewPlaceServer -> Failed to start new place server for Place {placeId}, unsupported place year got {UniverseObj.place_year.name}")
|
||||
raise UnsupportedPlaceYear(f"Year {UniverseObj.place_year.name} is not supported")
|
||||
|
||||
if UniverseObj.place_year in [ PlaceYear.Eighteen, PlaceYear.Twenty, PlaceYear.TwentyOne ]:
|
||||
redis_controller.set(f"GameServerAccessKey:{CommApiKey}:{JobId}", "1", ex=60*60*24*2)
|
||||
if UniverseObj.place_year in [ PlaceYear.Fourteen ]:
|
||||
redis_controller.set(f"gameserver2014lua:{PlaceObj.placeid}:{JobId}", "1", ex=120)
|
||||
|
||||
try:
|
||||
OpenJobReq = perform_post(
|
||||
TargetGameserver = SelectedGameServerObj,
|
||||
Endpoint = GameOpenRoute,
|
||||
JSONData = JSONOpenPayload,
|
||||
RequestTimeout = 35
|
||||
)
|
||||
except requests.exceptions.Timeout:
|
||||
logging.error(f"CreateNewPlaceServer -> Failed to start new place server for Place {placeId} on Gameserver {SelectedGameServerObj.serverName} ({SelectedGameServerObj.serverId}), open request timed out")
|
||||
raise NoAvailableGameServers(f"Request timed out")
|
||||
|
||||
if OpenJobReq.status_code != 200:
|
||||
logging.error(f"CreateNewPlaceServer -> Failed to start new place server for Place {placeId} on Gameserver {SelectedGameServerObj.serverName} ({SelectedGameServerObj.serverId}), response: {OpenJobReq.content}")
|
||||
raise UnexpectedStatusCode(f"Got status code {OpenJobReq.status_code} from Gameserver")
|
||||
|
||||
OpenJobReqJSON = OpenJobReq.json()
|
||||
if "jobid" not in OpenJobReqJSON or "port" not in OpenJobReqJSON:
|
||||
logging.error(f"CreateNewPlaceServer -> Failed to start new place server for Place {placeId} on Gameserver {SelectedGameServerObj.serverName} ({SelectedGameServerObj.serverId}), missing 'port' or 'jobid' in JSON,response: {OpenJobReqJSON}")
|
||||
raise BadResponseData(f"Missing 'port' or 'jobid' in JSON response")
|
||||
redis_controller.set(f"place:{OpenJobReqJSON['jobid']}:origin", str(SelectedGameServerObj.serverId), ex=60*60*24*2)
|
||||
PlaceServerObject = PlaceServer(
|
||||
serveruuid = OpenJobReqJSON["jobid"],
|
||||
originServerId = SelectedGameServerObj.serverId,
|
||||
serverIP = SelectedGameServerObj.serverIP,
|
||||
serverPort = OpenJobReqJSON["port"],
|
||||
serverPlaceId = placeId,
|
||||
maxPlayerCount = PlaceObj.maxplayers,
|
||||
reservedServerAccessCode = reserved_server_access_code
|
||||
)
|
||||
|
||||
db.session.add(PlaceServerObject)
|
||||
db.session.commit()
|
||||
logging.info(f"CreateNewPlaceServer -> Started new place server for Place {placeId}, Universe {UniverseObj.id} on Gameserver {SelectedGameServerObj.serverName} ({SelectedGameServerObj.serverId}), is_reserved: {reserved_server_access_code is not None}")
|
||||
|
||||
return PlaceServerObject
|
||||
|
||||
def GetSuitablePlaceServer( placeId : int ) -> PlaceServer | bool:
|
||||
"""
|
||||
Returns a suitable PlaceServer for the given placeId
|
||||
|
||||
:param placeId: The placeId to find a suitable PlaceServer for
|
||||
|
||||
:return: PlaceServer object if successful, returns False if no PlaceServer is found
|
||||
"""
|
||||
PlaceObj : Place = Place.query.filter_by( placeid = placeId ).first()
|
||||
if PlaceObj is None:
|
||||
logging.error(f"GetSuitablePlaceServer -> Failed to find Place {placeId} in database")
|
||||
raise MissingData("")
|
||||
|
||||
HasFoundSuitablePlaceServer : bool = False
|
||||
|
||||
PlaceServers = PlaceServer.query.filter_by( serverPlaceId = placeId, reservedServerAccessCode = None ).all()
|
||||
if PlaceServers is None or ( type(PlaceServers) == list and len(PlaceServers) == 0 ):
|
||||
HasFoundSuitablePlaceServer = False
|
||||
|
||||
if not HasFoundSuitablePlaceServer:
|
||||
for PlaceServerObject in PlaceServers:
|
||||
PlaceServerObject : PlaceServer
|
||||
if PlaceServerObject.maxPlayerCount <= PlaceServerObject.playerCount:
|
||||
continue
|
||||
if PlaceServerObject.serverRunningTime == 0:
|
||||
continue
|
||||
return PlaceServerObject
|
||||
|
||||
if HasFoundSuitablePlaceServer is False:
|
||||
logging.info(f"GetSuitablePlaceServer -> Place {placeId} has no PlaceServers, attempting to start one")
|
||||
try:
|
||||
NewPlaceServerObj = CreateNewPlaceServer( placeId = placeId, reserved_server_access_code = None )
|
||||
except:
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
GameJoinRoute = Blueprint('gamejoin', __name__, url_prefix='/')
|
||||
|
||||
@GameJoinRoute.route('/universes/validate-place-join', methods=['GET'])
|
||||
def validateplacejoin():
|
||||
return "true"
|
||||
|
||||
@GameJoinRoute.route('/Game/Join2012.ashx', methods=['GET'])
|
||||
def Join2012():
|
||||
SignedFirstTicketRaw : str = signUTF8("print('hello')", formatAutomatically=True, addNewLine=True, twelveclient=True)
|
||||
Resposne = make_response(SignedFirstTicketRaw)
|
||||
|
||||
return Resposne
|
||||
|
||||
@GameJoinRoute.route('/game/validate-machine', methods=['POST'])
|
||||
@csrf.exempt
|
||||
def validate_machine():
|
||||
try:
|
||||
if request.cookies.get("t") is None:
|
||||
raise Exception("Request has no 't' cookie")
|
||||
|
||||
macAddressesList = request.form.getlist('macAddresses')
|
||||
if len(macAddressesList) > 0:
|
||||
combinedAddress = ""
|
||||
for macAddress in macAddressesList:
|
||||
combinedAddress += macAddress
|
||||
UserHWIDHash = hashlib.sha256(combinedAddress.encode("utf-8")).hexdigest()
|
||||
tracking_cookie = request.cookies.get("t")
|
||||
redis_controller.setex(f"hwid:{str(tracking_cookie)}", 60, UserHWIDHash)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed during /game/validate-machine: {e}")
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": ""
|
||||
})
|
||||
|
||||
@GameJoinRoute.route('/Game/MachineConfiguration.ashx', methods=['POST', 'GET'])
|
||||
@csrf.exempt
|
||||
def machine_configuration():
|
||||
return ""
|
||||
|
||||
@GameJoinRoute.route('/v1/heartbeat', methods=['POST', 'GET'])
|
||||
@csrf.exempt
|
||||
def heartbeat():
|
||||
response = make_response(
|
||||
jsonify({
|
||||
"success": "true",
|
||||
})
|
||||
)
|
||||
return response
|
||||
|
||||
def ReturnPlaceLauncher( message : str, status : int, authenticated_userid : int = None) -> Response:
|
||||
response = make_response(
|
||||
jsonify({
|
||||
"jobId": None,
|
||||
"status": status,
|
||||
"joinScriptUrl": None,
|
||||
"authenticationUrl": config.BaseURL + "/Login/Negotiate.ashx",
|
||||
"authenticationTicket": None,
|
||||
"message": message,
|
||||
"rand": random.randint(0, 100000000000)
|
||||
})
|
||||
)
|
||||
|
||||
if authenticated_userid is not None and request.cookies.get(".ROBLOSECURITY") is None:
|
||||
response.set_cookie(
|
||||
key = ".ROBLOSECURITY",
|
||||
value = auth.CreateToken( userid = authenticated_userid, expireIn = 60 * 60 * 24 * 3, ip = get_remote_address() ),
|
||||
expires = datetime.utcnow() + timedelta(days=3),
|
||||
domain = f".{config.BaseDomain}"
|
||||
)
|
||||
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
||||
return response
|
||||
|
||||
@GameJoinRoute.route('/game/PlaceLauncher.ashx', methods=['GET', 'POST'])
|
||||
@GameJoinRoute.route('/Game/PlaceLauncher.ashx', methods=['GET', 'POST'])
|
||||
@GameJoinRoute.route('/game/placelauncher.ashx', methods=['GET', 'POST'])
|
||||
@GameJoinRoute.route('/Game/placelauncher.ashx', methods=['GET', 'POST'])
|
||||
@csrf.exempt
|
||||
def placelauncher():
|
||||
if not websiteFeatures.GetWebsiteFeature("GameJoinAPI"):
|
||||
return ReturnPlaceLauncher("GameJoinAPI is disabled", 12)
|
||||
|
||||
AuthenticatdUser = None
|
||||
placeid = request.args.get( key = 'placeId', default = None, type = int) or request.args.get( key = 'placeid', default = None, type = int)
|
||||
ticket = request.args.get( key = 't', default = None, type = str)
|
||||
if ticket is None:
|
||||
AuthenticatdUser : User | None = auth.GetCurrentUser()
|
||||
if AuthenticatdUser is None:
|
||||
return ReturnPlaceLauncher("Invalid request", 12)
|
||||
if placeid is None:
|
||||
return ReturnPlaceLauncher("Invalid request", 12)
|
||||
|
||||
requestedJobId = request.args.get( key = 'jobId', default = None, type = str) or request.args.get( key = 'jobid', default = None, type = str)
|
||||
isTeleport = request.args.get( key = 'isTeleport', default = None, type = str ) == "true"
|
||||
requestType = request.args.get( key = 'request', default = "RequestGame", type = str )
|
||||
|
||||
if request.user_agent != "Roblox/WinInet":
|
||||
isTeleport = False
|
||||
|
||||
authticketInfo = redis_controller.get(f"authticket:{ticket}")
|
||||
if authticketInfo is None and AuthenticatdUser is None:
|
||||
return ReturnPlaceLauncher("Invalid authentication ticket", 12)
|
||||
|
||||
#UserIPHash = hashlib.md5(get_remote_address().encode("utf-8")).hexdigest()
|
||||
#LoginRecords : list[LoginRecord] = LoginRecord.query.filter(LoginRecord.ip == UserIPHash).distinct(LoginRecord.userid).all()
|
||||
#for record in LoginRecords:
|
||||
# if record.User.accountstatus != 1:
|
||||
# return ReturnPlaceLauncher("Invalid authentication ticket", 12)
|
||||
|
||||
userId = int(authticketInfo) if authticketInfo is not None else AuthenticatdUser.id
|
||||
if PlaceServerPlayer.query.filter_by(userid=userId).first() is not None and not isTeleport:
|
||||
CurrentPlaceServerPlayerObj : PlaceServerPlayer = PlaceServerPlayer.query.filter_by(userid=userId).first()
|
||||
CurrentPlaceServerObj : PlaceServer = PlaceServer.query.filter_by(serveruuid=CurrentPlaceServerPlayerObj.serveruuid).first()
|
||||
try:
|
||||
EvictPlayer(CurrentPlaceServerObj, userId)
|
||||
except:
|
||||
return ReturnPlaceLauncher("Invalid request", 12)
|
||||
|
||||
AssetObj : Asset = Asset.query.filter_by(id=placeid).first()
|
||||
if AssetObj is None:
|
||||
return ReturnPlaceLauncher("Invalid place", 14)
|
||||
if AssetObj.asset_type != AssetType.Place or AssetObj.moderation_status == 2:
|
||||
return ReturnPlaceLauncher("Invalid place", 14)
|
||||
PlaceObj : Place = Place.query.filter_by(placeid=placeid).first()
|
||||
if PlaceObj is None:
|
||||
return ReturnPlaceLauncher("Invalid place", 14)
|
||||
UniverseObj : Universe = Universe.query.filter_by(id=PlaceObj.parent_universe_id).first()
|
||||
if UniverseObj.is_public == False and userId != 1:
|
||||
return ReturnPlaceLauncher("Invalid place", 14)
|
||||
|
||||
UserObj : User = User.query.filter_by(id=userId).first()
|
||||
|
||||
if request.cookies.get("t") is not None:
|
||||
Tracking_Cookie = request.cookies.get("t")
|
||||
UserHWIDHash = redis_controller.get(f"hwid:{str(Tracking_Cookie)}")
|
||||
if UserHWIDHash is not None:
|
||||
UserHWIDLogObject = UserHWIDLog(
|
||||
user_id = UserObj.id,
|
||||
hwid = UserHWIDHash
|
||||
)
|
||||
db.session.add(UserHWIDLogObject)
|
||||
db.session.commit()
|
||||
|
||||
redis_controller.delete(f"hwid:{str(Tracking_Cookie)}")
|
||||
|
||||
UserMembershipStatus : MembershipType = GetUserMembership(UserObj)
|
||||
if UniverseObj.bc_required and UserMembershipStatus == MembershipType.NonBuildersClub:
|
||||
return ReturnPlaceLauncher("Builders Club required", 12)
|
||||
if UniverseObj.minimum_account_age > (datetime.utcnow() - UserObj.created).days:
|
||||
return ReturnPlaceLauncher("Account is too new to join this place", 12)
|
||||
|
||||
if requestedJobId is not None:
|
||||
PlaceServerObj : PlaceServer = PlaceServer.query.filter_by( serveruuid = requestedJobId, serverPlaceId = placeid, reservedServerAccessCode = None ).first()
|
||||
if PlaceServerObj is None:
|
||||
return ReturnPlaceLauncher("Invalid request", 12)
|
||||
if PlaceServerObj.maxPlayerCount <= PlaceServerObj.playerCount:
|
||||
return ReturnPlaceLauncher("Server is full", 6, authenticated_userid=userId)
|
||||
elif requestType == "RequestPrivateGame":
|
||||
server_accessCode = request.args.get( key = 'accessCode', default = None, type = str)
|
||||
if server_accessCode is None:
|
||||
return ReturnPlaceLauncher("Invalid request", 12)
|
||||
PlaceServerObj : PlaceServer = PlaceServer.query.filter_by(serverPlaceId = placeid, reservedServerAccessCode = server_accessCode ).first()
|
||||
if PlaceServerObj is None:
|
||||
return ReturnPlaceLauncher("Invalid request", 12)
|
||||
if redis_controller.exists(f"reservedserveraccesscode:{str(PlaceServerObj.serveruuid)}:{UserObj.id}") is False:
|
||||
return ReturnPlaceLauncher("Invalid request", 12)
|
||||
if PlaceServerObj.maxPlayerCount <= PlaceServerObj.playerCount:
|
||||
return ReturnPlaceLauncher("Server is full", 6, authenticated_userid=userId)
|
||||
|
||||
redis_controller.delete(f"reservedserveraccesscode:{str(PlaceServerObj.serveruuid)}:{UserObj.id}")
|
||||
else:
|
||||
PlaceServerObj : PlaceServer | bool = GetSuitablePlaceServer( placeId = placeid )
|
||||
|
||||
if PlaceServerObj is False:
|
||||
logging.info(f"Placelauncher.ashx : {str(placeid)} : {UserObj.username} [{UserObj.id}] : No available place servers found yet")
|
||||
return ReturnPlaceLauncher(None, 1, authenticated_userid=userId)
|
||||
redis_controller.delete(f"authticket:{ticket}")
|
||||
authenticatedTicketUUID = str(uuid.uuid4())
|
||||
redis_controller.setex(f"place:{placeid}:ticket:{authenticatedTicketUUID}", 60, json.dumps({"id": userId, "jobid": str(PlaceServerObj.serveruuid)}))
|
||||
|
||||
authticket = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(256))
|
||||
redis_controller.set(f"authticket:{authticket}", userId, 60*10)
|
||||
resp = make_response(jsonify({
|
||||
"jobId": PlaceServerObj.serveruuid,
|
||||
"status": 2,
|
||||
"joinScriptUrl": config.BaseURL + "/Game/Join.ashx?placeId=" + str(placeid) + "&jobId=" + str(PlaceServerObj.serveruuid) + "&ticket=" + authenticatedTicketUUID,
|
||||
"authenticationUrl": config.BaseURL + "/Login/Negotiate.ashx",
|
||||
"authenticationTicket": authticket,
|
||||
"message": None,
|
||||
"rand": random.randint(0, 100000000000)
|
||||
}))
|
||||
resp.set_cookie("ticket", authenticatedTicketUUID)
|
||||
if request.cookies.get(".ROBLOSECURITY") is None:
|
||||
resp.set_cookie(
|
||||
key = ".ROBLOSECURITY",
|
||||
value = auth.CreateToken( userid = userId, expireIn = 60 * 60 * 24 * 3, ip = get_remote_address() ),
|
||||
expires = datetime.utcnow() + timedelta(days=3),
|
||||
domain = f".{config.BaseDomain}"
|
||||
)
|
||||
return resp
|
||||
|
||||
@GameJoinRoute.route("/v1/join-game", methods=["POST"]) # Meant for 2020 Android
|
||||
@csrf.exempt
|
||||
@auth.authenticated_required_api
|
||||
def gamejoin_api_v1():
|
||||
if not websiteFeatures.GetWebsiteFeature("GameJoinAPI"):
|
||||
return ReturnPlaceLauncher("GameJoinAPI is disabled", 12)
|
||||
if not request.is_json:
|
||||
return ReturnPlaceLauncher("Invalid request", 12)
|
||||
if "placeId" not in request.json:
|
||||
return ReturnPlaceLauncher("Invalid request", 12)
|
||||
try:
|
||||
requestedPlaceId : int = int(request.json["placeId"])
|
||||
isTeleport : bool = request.json["isTeleport"] if "isTeleport" in request.json else False
|
||||
except:
|
||||
return ReturnPlaceLauncher("Invalid request", 12)
|
||||
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
if AuthenticatedUser is None: # Shouldnt happen but just in case
|
||||
return ReturnPlaceLauncher("Invalid request", 12)
|
||||
|
||||
userId = AuthenticatedUser.id
|
||||
if PlaceServerPlayer.query.filter_by(userid=userId).first() is not None and not isTeleport:
|
||||
CurrentPlaceServerPlayerObj : PlaceServerPlayer = PlaceServerPlayer.query.filter_by(userid=userId).first()
|
||||
CurrentPlaceServerObj : PlaceServer = PlaceServer.query.filter_by(serveruuid=CurrentPlaceServerPlayerObj.serveruuid).first()
|
||||
try:
|
||||
EvictPlayer(CurrentPlaceServerObj, userId)
|
||||
except:
|
||||
logging.error(f"/v1/join-game - Failed to evict player {userId} from place {CurrentPlaceServerObj.serverPlaceId}")
|
||||
return ReturnPlaceLauncher("Invalid request", 12)
|
||||
|
||||
AssetObj : Asset = Asset.query.filter_by(id=requestedPlaceId).first()
|
||||
if AssetObj is None:
|
||||
return ReturnPlaceLauncher("Invalid place", 14)
|
||||
if AssetObj.asset_type != AssetType.Place or AssetObj.moderation_status == 2:
|
||||
return ReturnPlaceLauncher("Invalid place", 14)
|
||||
PlaceObj : Place = Place.query.filter_by(placeid=requestedPlaceId).first()
|
||||
if PlaceObj is None:
|
||||
return ReturnPlaceLauncher("Invalid place", 14)
|
||||
UniverseObj : Universe = Universe.query.filter_by(id=PlaceObj.parent_universe_id).first()
|
||||
if UniverseObj.is_public == False and userId != 1:
|
||||
return ReturnPlaceLauncher("Invalid place", 14)
|
||||
|
||||
UserMembershipStatus : MembershipType = GetUserMembership(AuthenticatedUser)
|
||||
if UniverseObj.bc_required and UserMembershipStatus == MembershipType.NonBuildersClub:
|
||||
return ReturnPlaceLauncher("Builders Club required", 12)
|
||||
if UniverseObj.minimum_account_age > (datetime.utcnow() - AuthenticatedUser.created).days:
|
||||
return ReturnPlaceLauncher("Account is too new to join this place", 12)
|
||||
|
||||
PlaceServerObj : PlaceServer | bool = GetSuitablePlaceServer( placeId = requestedPlaceId )
|
||||
|
||||
if PlaceServerObj is False:
|
||||
logging.info(f"/v1/join-game : {str(requestedPlaceId)} : {AuthenticatedUser.username} [{AuthenticatedUser.id}] : No available place servers found yet")
|
||||
return ReturnPlaceLauncher(None, 1, authenticated_userid=userId)
|
||||
|
||||
ClientTicket = GenerateClientTicket(AuthenticatedUser, PlaceServerObj.serveruuid, TicketVersion = 4, PlaceId = requestedPlaceId)
|
||||
SessionId : str = f"{str(uuid.uuid4())}|{str(PlaceServerObj.serveruuid)}|0|{str(PlaceServerObj.serverIP)}|8|{datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.000Z')}|0|null|AAAAA"
|
||||
|
||||
resp = make_response(jsonify({
|
||||
"jobId": PlaceServerObj.serveruuid,
|
||||
"status": 2,
|
||||
"authenticationUrl": config.BaseURL + "/Login/Negotiate.ashx",
|
||||
"authenticationTicket": "",
|
||||
"message": None,
|
||||
"rand": random.randint(0, 100000000000),
|
||||
"joinScript": {
|
||||
"ClientPort" : 0,
|
||||
"MachineAddress" : PlaceServerObj.serverIP,
|
||||
"ServerConnections": [
|
||||
{
|
||||
"Port": PlaceServerObj.serverPort,
|
||||
"Address": PlaceServerObj.serverIP
|
||||
}
|
||||
],
|
||||
"ServerPort" : PlaceServerObj.serverPort,
|
||||
"PingUrl": "",
|
||||
"PingInterval": 120,
|
||||
"UserName": AuthenticatedUser.username,
|
||||
"DisplayName": AuthenticatedUser.username,
|
||||
"SeleniumTestMode": False,
|
||||
"UserId": AuthenticatedUser.id,
|
||||
"ClientTicket": "",
|
||||
"SuperSafeChat": False,
|
||||
"PlaceId": PlaceServerObj.serverPlaceId,
|
||||
"MeasurementUrl": "",
|
||||
"WaitingForCharacterGuid": str(uuid.uuid4()),
|
||||
"BaseUrl": config.BaseURL,
|
||||
"ChatStyle": PlaceObj.chat_style.name,
|
||||
"VendorId": 0,
|
||||
"ScreenShotInfo": "",
|
||||
"VideoInfo": "",
|
||||
"CreatorId": AssetObj.creator_id,
|
||||
"CreatorTypeEnum": "User" if AssetObj.creator_type == 0 else "Group",
|
||||
"MembershipType": GetUserMembership(AuthenticatedUser, changeToString=True),
|
||||
"AccountAge": (datetime.utcnow() - AuthenticatedUser.created).days,
|
||||
"CookieStoreFirstTimePlayKey": "rbx_evt_ftp",
|
||||
"CookieStoreFiveMinutePlayKey": "rbx_evt_fmp",
|
||||
"CookieStoreEnabled": True,
|
||||
"IsRobloxPlace": False,
|
||||
"UniverseId": PlaceObj.parent_universe_id,
|
||||
"GenerateTeleportJoin": False,
|
||||
"IsUnknownOrUnder13": False,
|
||||
"SessionId": SessionId,
|
||||
"DataCenterId": 0,
|
||||
"FollowUserId": 0,
|
||||
"BrowserTrackerId": 0,
|
||||
"UsePortraitMode": False,
|
||||
"CharacterAppearance": f"http://www.vortexi.cc/v1/avatar-fetch?userId={str(AuthenticatedUser.id)}&placeId={str(requestedPlaceId)}",
|
||||
"GameId": PlaceServerObj.serverPlaceId,
|
||||
"RobloxLocale": "en_us",
|
||||
"GameLocale": "en_us",
|
||||
"characterAppearanceId": AuthenticatedUser.id
|
||||
}
|
||||
}))
|
||||
return resp
|
||||
|
||||
def GenerateClientTicket( UserObj : User, JobId : str, CharacterURL : str = None, CustomTimestamp : str = "", TicketVersion : int = 1, PlaceId : int = 1) -> str:
|
||||
"""
|
||||
Generates a client ticket so that RCC can verify the user is authenticated
|
||||
If CharacterURL is not None, it will be used as the character URL instead of the default
|
||||
If CustomTimestamp is not 0, it will be used as the timestamp instead of the current time
|
||||
"""
|
||||
if CustomTimestamp == "":
|
||||
CustomTimestamp = datetime.utcnow().strftime("%m/%d/%Y %I:%M:%S %p")
|
||||
if CharacterURL is None:
|
||||
if TicketVersion == 2:
|
||||
CharacterURL = str(UserObj.id)
|
||||
elif TicketVersion == 1:
|
||||
CharacterURL = Config.BaseURL + "/Asset/CharacterFetch.ashx?userId=" + str(UserObj.id) # f"http://www.vortexi.cc/v1.1/avatar-fetch?userId={str(UserObj.id)}&placeId={str(PlaceId)}"
|
||||
elif TicketVersion == 4:
|
||||
CharacterURL = f"http://www.vortexi.cc/v1/avatar-fetch?userId={str(UserObj.id)}&placeId={str(PlaceId)}"
|
||||
|
||||
FirstTicketUnsigned = f"{str(UserObj.id)}\n{UserObj.username}\n{CharacterURL}\n{JobId}\n{str(CustomTimestamp)}"
|
||||
SignedFirstTicketRaw : bytes = signUTF8(FirstTicketUnsigned, formatAutomatically=False, addNewLine=False, useNewKey=(TicketVersion > 1))
|
||||
SignedFirstTicket = base64.b64encode(SignedFirstTicketRaw).decode("utf-8")
|
||||
|
||||
AccountAge = (datetime.utcnow() - UserObj.created).days
|
||||
UserMembershipType = GetUserMembership(UserObj, changeToString=True)
|
||||
|
||||
if TicketVersion <= 3:
|
||||
SecondTicketUnsigned = f"{str(UserObj.id)}\n{str(JobId)}\n{str(CustomTimestamp)}"
|
||||
elif TicketVersion == 4:
|
||||
SecondTicketUnsigned = f"{CustomTimestamp}\n{JobId}\n{UserObj.id}\n{UserObj.id}\n0\n{AccountAge}\nf\n{len(UserObj.username)}\n{UserObj.username}\n{len(UserMembershipType)}\n{UserMembershipType}\n0\n\n0\n\n{len(UserObj.username)}\n{UserObj.username}"
|
||||
|
||||
SignedSecondTicketRaw : bytes = signUTF8(SecondTicketUnsigned, formatAutomatically=False, addNewLine=False, useNewKey=(TicketVersion > 1))
|
||||
SignedSecondTicket = base64.b64encode(SignedSecondTicketRaw).decode("utf-8")
|
||||
|
||||
return f"{str(CustomTimestamp)};{SignedFirstTicket};{SignedSecondTicket}{f';{TicketVersion}' if TicketVersion > 1 else ''}"
|
||||
|
||||
@GameJoinRoute.route('/Game/Join.ashx', methods=['GET', 'POST'])
|
||||
@csrf.exempt
|
||||
def join():
|
||||
placeid = request.args.get('placeId', default = None, type = int)
|
||||
jobid = request.args.get('jobId', default = None, type = str)
|
||||
ticket = request.args.get('ticket', default = None, type = str)
|
||||
if placeid is None or jobid is None or ticket is None:
|
||||
return 'Invalid request ( 0 )',400
|
||||
|
||||
ticketInfo = redis_controller.get(f"place:{placeid}:ticket:{ticket}")
|
||||
if ticketInfo is None:
|
||||
return 'Invalid request ( 1 )',400
|
||||
ticketInfo = json.loads(ticketInfo)
|
||||
if ticketInfo['jobid'] != jobid:
|
||||
return 'Invalid request ( 2 )',400
|
||||
|
||||
PlaceServerObj : PlaceServer = PlaceServer.query.filter_by(serveruuid=jobid).first()
|
||||
if PlaceServerObj is None:
|
||||
return 'Invalid request ( 3 )',400
|
||||
if PlaceServerObj.serverPlaceId != placeid:
|
||||
return 'Invalid request ( 4 )',400
|
||||
if PlaceServerObj.serverRunningTime == 0:
|
||||
return 'Invalid request ( 5 )',400
|
||||
if PlaceServerObj.maxPlayerCount <= PlaceServerObj.playerCount:
|
||||
return 'Invalid request ( 6 )',400
|
||||
UserObj : User = User.query.filter_by(id=ticketInfo['id']).first()
|
||||
if UserObj is None:
|
||||
return 'Invalid request ( 7 )',400
|
||||
PlaceObj : Place = Place.query.filter_by(placeid=placeid).first()
|
||||
AssetObj : Asset = Asset.query.filter_by(id=placeid).first()
|
||||
UniverseObj : Universe = Universe.query.filter_by(id=PlaceObj.parent_universe_id).first()
|
||||
|
||||
ClientTicket = GenerateClientTicket(UserObj, jobid, TicketVersion = 1 if UniverseObj.place_year in [PlaceYear.Sixteen, PlaceYear.Fourteen] else ( 2 if UniverseObj.place_year == PlaceYear.Eighteen else 4), PlaceId = placeid)
|
||||
|
||||
AuthenticationTicket = auth.CreateToken(UserObj.id, get_remote_address() , (60*60*24) )
|
||||
SessionId : str = f"{str(uuid.uuid4())}|{str(PlaceServerObj.serveruuid)}|0|{str(PlaceServerObj.serverIP)}|8|{datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.000Z')}|0|null|{AuthenticationTicket}"
|
||||
|
||||
UserMembershipStatus : str = GetUserMembership(UserObj, changeToString=True)
|
||||
JoinData : str = json.dumps({
|
||||
"ClientPort" : 0,
|
||||
"MachineAddress" : PlaceServerObj.serverIP,
|
||||
"ServerConnections": [
|
||||
{
|
||||
"Port": PlaceServerObj.serverPort,
|
||||
"Address": PlaceServerObj.serverIP
|
||||
}
|
||||
],
|
||||
"ServerPort" : PlaceServerObj.serverPort,
|
||||
"PingUrl": "",
|
||||
"PingInterval": 120,
|
||||
"UserName": UserObj.username,
|
||||
"DisplayName": UserObj.username,
|
||||
"SeleniumTestMode": False,
|
||||
"UserId": UserObj.id,
|
||||
"ClientTicket": "",
|
||||
"SuperSafeChat": False,
|
||||
"PlaceId": PlaceServerObj.serverPlaceId,
|
||||
"MeasurementUrl": "",
|
||||
"WaitingForCharacterGuid": str(uuid.uuid4()),
|
||||
"BaseUrl": config.BaseURL,
|
||||
"ChatStyle": PlaceObj.chat_style.name,
|
||||
"VendorId": 0,
|
||||
"ScreenShotInfo": "",
|
||||
"VideoInfo": "",
|
||||
"CreatorId": AssetObj.creator_id,
|
||||
"CreatorTypeEnum": "User" if AssetObj.creator_type == 0 else "Group",
|
||||
"MembershipType": UserMembershipStatus,
|
||||
"AccountAge": (datetime.utcnow() - UserObj.created).days,
|
||||
"CookieStoreFirstTimePlayKey": "rbx_evt_ftp",
|
||||
"CookieStoreFiveMinutePlayKey": "rbx_evt_fmp",
|
||||
"CookieStoreEnabled": True,
|
||||
"IsRobloxPlace": False,
|
||||
"UniverseId": PlaceObj.parent_universe_id,
|
||||
"GenerateTeleportJoin": False,
|
||||
"IsUnknownOrUnder13": False,
|
||||
"SessionId": SessionId,
|
||||
"DataCenterId": 0,
|
||||
"FollowUserId": 0,
|
||||
"BrowserTrackerId": 0,
|
||||
"UsePortraitMode": False,
|
||||
"CharacterAppearance": config.BaseURL + "/Asset/CharacterFetch.ashx?userId=" + str(UserObj.id) if PlaceObj.placeyear in [PlaceYear.Fourteen, PlaceYear.Sixteen] else ( f"http://www.vortexi.cc/v1.1/avatar-fetch?userId={str(UserObj.id)}&placeId={str(placeid)}" if PlaceObj.placeyear in [PlaceYear.Eighteen] else f"http://www.vortexi.cc/v1/avatar-fetch?userId={str(UserObj.id)}&placeId={str(placeid)}" ),
|
||||
"GameId": PlaceServerObj.serverPlaceId,
|
||||
"RobloxLocale": "en_us",
|
||||
"GameLocale": "en_us",
|
||||
"characterAppearanceId": UserObj.id
|
||||
})
|
||||
if UniverseObj.place_year == PlaceYear.Sixteen:
|
||||
SignedJoinData : str = signUTF8("\r\n"+JoinData, addNewLine=False)
|
||||
elif UniverseObj.place_year == PlaceYear.Eighteen:
|
||||
SignedJoinData : str = signUTF8("\r\n"+JoinData, addNewLine=False, useNewKey=True)
|
||||
elif UniverseObj.place_year == PlaceYear.Twenty:
|
||||
SignedJoinData : str = signUTF8("\r\n"+JoinData, addNewLine=False, useNewKey=True)
|
||||
elif UniverseObj.place_year == PlaceYear.Fourteen:
|
||||
JoinData = json.loads(JoinData)
|
||||
VerificationTicket = str(uuid.uuid4())
|
||||
JoinData["CharacterAppearance"] = f"{config.BaseURL}/Asset/CharacterFetch.ashx?userId={str(UserObj.id)}&t={VerificationTicket}&legacy=1"
|
||||
redis_controller.set(
|
||||
f"joinashx-auth:{str(jobid)}:{str(UserObj.id)}:{placeid}:{VerificationTicket}",
|
||||
json.dumps({
|
||||
"CharacterAppearance": JoinData["CharacterAppearance"],
|
||||
"Username": JoinData["UserName"]
|
||||
}),
|
||||
ex = 60
|
||||
)
|
||||
JoinData = json.dumps(JoinData)
|
||||
|
||||
SignedJoinData : str = signUTF8("\r\n"+JoinData, addNewLine=False)
|
||||
elif UniverseObj.place_year == PlaceYear.TwentyOne:
|
||||
SignedJoinData : str = signUTF8("\r\n"+JoinData, addNewLine=False, useNewKey=True)
|
||||
else:
|
||||
return 'Invalid request ( 8 )',400
|
||||
|
||||
if UniverseObj.place_year in [ PlaceYear.Fourteen, PlaceYear.Sixteen ]:
|
||||
redis_controller.set(f"allow_join:{str(UserObj.id)}:{str(placeid)}:{str(jobid)}", "1", ex=120)
|
||||
|
||||
joinResposne = make_response(SignedJoinData)
|
||||
joinResposne.headers['Content-Type'] = 'text/plain'
|
||||
joinResposne.set_cookie(
|
||||
key = "Syntax-Session-Id",
|
||||
value = SessionId,
|
||||
expires = datetime.utcnow() + timedelta(days=1),
|
||||
domain = f".{config.BaseDomain}"
|
||||
)
|
||||
if request.cookies.get(".ROBLOSECURITY") is None: # 2018 and 2020 doesen't want to authenticate properly with negotiate.ashx :(, so this is my hack till i can figure out why
|
||||
joinResposne.set_cookie(
|
||||
key = ".ROBLOSECURITY",
|
||||
value = auth.CreateToken( userid = UserObj.id, expireIn = 60 * 60 * 24 * 3, ip = get_remote_address() ),
|
||||
expires = datetime.utcnow() + timedelta(days=3),
|
||||
domain = f".{config.BaseDomain}"
|
||||
)
|
||||
return joinResposne
|
||||
Reference in New Issue
Block a user