714 lines
34 KiB
Python
714 lines
34 KiB
Python
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 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 ""
|
|
|
|
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(random.choices(string.ascii_uppercase + string.digits, k=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": 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.syntax.eco/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.syntax.eco/v1.1/avatar-fetch?userId={str(UserObj.id)}&placeId={str(PlaceId)}"
|
|
elif TicketVersion == 4:
|
|
CharacterURL = f"http://www.syntax.eco/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": 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.syntax.eco/v1.1/avatar-fetch?userId={str(UserObj.id)}&placeId={str(placeid)}" if PlaceObj.placeyear in [PlaceYear.Eighteen] else f"http://www.syntax.eco/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 |