422 lines
21 KiB
Python
422 lines
21 KiB
Python
from .packet import Packet
|
|
from .EnumQuery import EnumQuery
|
|
from .EnumResponse import EnumResponse
|
|
from .CFrame import CFrame
|
|
from .DFrame import DFrame
|
|
from giants.masterserver import MasterServer
|
|
from .session import Session
|
|
import socket
|
|
import threading
|
|
import traceback
|
|
import uuid
|
|
import struct
|
|
from _datetime import datetime
|
|
from utils.logger import setup_logger
|
|
from giants.player import Player, PlayerPhases
|
|
import time
|
|
from giants import APPLICATION_GUID
|
|
from .DN_MSG_INTERNAL_SEND_CONNECT_INFO import DN_MSG_INTERNAl_SEND_CONNECT_INFO
|
|
import asyncio
|
|
|
|
logger = setup_logger(__name__)
|
|
|
|
|
|
class Netserver(asyncio.DatagramProtocol):
|
|
def __init__(self, server):
|
|
self.remotesocket = None
|
|
self.statssocket = None
|
|
self.server = server
|
|
self.addrs = []
|
|
self.guid = uuid.uuid4().bytes
|
|
self.incoming_packets = []
|
|
super().__init__()
|
|
|
|
def connection_made(self, transport):
|
|
logger.debug("Connection made")
|
|
self.remotesocket = transport
|
|
|
|
def run(self):
|
|
logger.debug("Run")
|
|
return
|
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
self.socket.bind((self.server.listen_ip, self.server.listen_port))
|
|
logger.debug("Listening to %s:%s", self.server.listen_ip, self.server.listen_port)
|
|
networkthread = threading.Thread(target=self.handle_packets)
|
|
networkthread.start()
|
|
|
|
if self.server.register_with_ms:
|
|
ms = MasterServer(self.server)
|
|
ms.register()
|
|
|
|
while self.server.running:
|
|
start = datetime.now()
|
|
self.read_packets()
|
|
self.server.update()
|
|
self.send_packets()
|
|
sleep_ms = (start-datetime.now()).total_seconds()+1/self.server.ticks
|
|
if sleep_ms > 0:
|
|
time.sleep(sleep_ms)
|
|
|
|
networkthread.join()
|
|
|
|
def read_packets(self):
|
|
pass
|
|
|
|
def send_packets(self):
|
|
pass
|
|
|
|
def datagram_received(self, data, addr):
|
|
logger.debug("%s:%s > %s", addr[0], addr[1], data.hex())
|
|
loop = asyncio.get_event_loop()
|
|
loop.create_task(self.handle_new_packet(addr, data))
|
|
|
|
async def handle_new_packet(self, addr, bData):
|
|
pPacket = Packet(bData)
|
|
first = pPacket.getByte()
|
|
if len(bData) >= 4 and first & DFrame.PACKET_COMMAND_DATA:
|
|
await self.handle_new_dframe(addr, pPacket)
|
|
elif len(bData) >= 12 and first & CFrame.PACKET_COMMAND_FRAME:
|
|
await self.handle_new_cframe(addr, pPacket)
|
|
elif first == EnumQuery.LEAD or first == EnumResponse.LEAD:
|
|
await self.handle_new_enum(addr, pPacket)
|
|
else:
|
|
logger.error("Unknown frame received")
|
|
return
|
|
|
|
async def handle_new_enum(self, addr, data):
|
|
# EnumQuery or EnumResponse
|
|
second = data.getByte()
|
|
if second == EnumQuery.COMMAND:
|
|
# EnumQuery
|
|
try:
|
|
eq = EnumQuery(data)
|
|
logger.debug("Got EnumQuery, sending EnumResponse")
|
|
er = EnumResponse()
|
|
er.Payload = eq.Payload
|
|
er.ApplicationDescSize = 0x50
|
|
er.ApplicationDescFlags = 0x41
|
|
er.MaxPlayers = self.server.maxplayers
|
|
er.CurrentPlayers = len(self.server.players)
|
|
er.SessionName = self.server.name
|
|
er.ApplicationInstanceGUID = self.guid
|
|
er.ApplicationGUID = eq.ApplicationGUID
|
|
'''er.ApplicationReservedData = b'\xff' # Map ID
|
|
er.ApplicationReservedData += b'\x00\x04\x00' # game type and teams
|
|
er.ApplicationReservedData += struct.pack("<L", int(self.server.version*1000)) # game version
|
|
er.ApplicationReservedData += b'\x02\x92\x05\x00\x01\x00\x00\x00\x00\x00' # Unknown
|
|
er.ApplicationReservedData += b'\x9c\x53\xf4\xdd' # Seems to be a checksum of current map
|
|
# \x00\x00\x00\x00: [None]
|
|
# \x00\x00\x00\x01: <custom map>
|
|
# \x9c\x53\xf4\xdd: Three Way Island - Canyons
|
|
# \x1e\xe9\x39\xe1: Still Winter
|
|
er.ApplicationReservedData += self.server.currentmap.mapname.encode("ascii")'''
|
|
|
|
appdata = Packet()
|
|
appdata.putByte(0xff) # map ID
|
|
appdata.putByte(self.server.game_type) # game type
|
|
appdata.putByte(self.server.teams) # teams
|
|
appdata.write(b'\x00') # original: 0x00 - does not seem to affect client
|
|
appdata.putShort(int(self.server.version * 1000))
|
|
appdata.write(b'\x02\x92') # original: 0292 - does not seem to affect client
|
|
appdata.putShort(self.server.points_per_capture)
|
|
appdata.putShort(self.server.points_per_kill)
|
|
appdata.write(b'\x00\x00') # original: 0000 - does not seem to affect client
|
|
appdata.putShort(self.server.detente_time)
|
|
appdata.write(
|
|
b'\x9c\x53\xf4\xdd') # Seems to be a checksum of current map OR linked to the number of chars in the map name
|
|
appdata.write(self.server.currentmap.mapname.encode("ascii"))
|
|
appdata.write(b'\x00' * (32 - len(self.server.currentmap.mapname)))
|
|
er.ApplicationReservedData = appdata.getvalue()
|
|
|
|
logger.debug("Current map: %s", self.server.currentmap.mapname)
|
|
er.ApplicationReservedData += b'\x00' * (32 - len(self.server.currentmap.mapname))
|
|
self.send_packet(addr, er.to_packet())
|
|
except Exception:
|
|
logger.error("Could not parse EnumQuery or forge EnumResponse: ")
|
|
traceback.print_exc()
|
|
return
|
|
elif second == EnumResponse.COMMAND:
|
|
# wait what? ignore that shit
|
|
return
|
|
else:
|
|
logger.error("Unknown DPLHP command: %s", second)
|
|
|
|
async def handle_new_cframe(self, addr, data):
|
|
try:
|
|
cframe = CFrame(data)
|
|
session = self.get_session(addr)
|
|
player = self.get_player(session) if session else None
|
|
if cframe.ExtOpCode == CFrame.FRAME_EXOPCODE_CONNECT:
|
|
# CONNECT CFRAME
|
|
if session and session.Full:
|
|
logger.error("Session %s:%s was already fully connected. Ignoring.", session.ip, session.port)
|
|
return
|
|
elif session and not session.Full and session.SessID == cframe.SessID:
|
|
# send CONNECTED
|
|
logger.debug("Already partially established connection. Sending a CFRAME CONNECTED again.")
|
|
session.send_cframe_connected(cframe)
|
|
#session.setup_Connect_Retry_Timer()
|
|
return
|
|
if not self.server.accept_new_players:
|
|
# ignore new players
|
|
return
|
|
else:
|
|
logger.debug("New connection. Sending a CFRAME CONNECTED.")
|
|
session = Session(self.remotesocket)
|
|
session.SessID = cframe.SessID
|
|
session.ip = addr[0]
|
|
session.port = addr[1]
|
|
await session.lock.acquire()
|
|
self.addrs.append(session)
|
|
session.send_cframe_connected(cframe)
|
|
player = Player("Unknown", session)
|
|
|
|
def bxor(b1, b2): # use xor for bytes
|
|
result = bytearray()
|
|
for b1, b2 in zip(b1, b2):
|
|
result.append(b1 ^ b2)
|
|
return result
|
|
player.id = struct.unpack(">L", bxor(struct.pack("<L", len(self.server.players)), self.guid[0:4]))[0]
|
|
logger.debug("Value for connected player: %s", player.id)
|
|
player.phase = PlayerPhases.CFRAME_CONNECT
|
|
self.server.create_temp_player(player)
|
|
#session.setup_Connect_Retry_Timer()
|
|
elif cframe.ExtOpCode == CFrame.FRAME_EXOPCODE_CONNECTED:
|
|
# CONNECTED CFRAME
|
|
# check if already sent a CONNECT packet
|
|
logger.debug("%s:%s sent back a CONNECTED CFrame.", session.ip, session.port)
|
|
if not session:
|
|
logger.error("%s sent a CONNECTED opcode without having sent a CONNECT before. GTFO.", addr)
|
|
return
|
|
|
|
if not cframe.SessID == session.SessID or cframe.Command & CFrame.PACKET_COMMAND_POLL:
|
|
logger.error("Sent a CONNECTED packet with incorrect SessID or had COMMAND_POLL")
|
|
return
|
|
session.Full = True # fully connected
|
|
session.cancel_Connect_Retry_Timer()
|
|
player.phase = PlayerPhases.CFRAME_CONNECTED
|
|
session.send_dframe_keepalive()
|
|
|
|
elif cframe.ExtOpCode == CFrame.FRAME_EXOPCODE_SACK:
|
|
if not session or not session.Full:
|
|
logger.error("Received a SACK packet for a non fully connected session")
|
|
return
|
|
|
|
sack_sent = False
|
|
if not cframe.NSeq == session.next_expected_seq:
|
|
logger.error("Received CFRAME (%s) does not have the same NSeq (%s). Did we miss a packet?", cframe.NSeq, session.next_expected_seq)
|
|
if not sack_sent:
|
|
session.send_cframe_sack()
|
|
sack_sent = True
|
|
|
|
if not session.next_send == cframe.NRecv:
|
|
logger.error("Received CFRAME (%s) does not have same NRcv (%s). One sent packet might have been lost.", cframe.NRecv, session.CFRAME_NRcv)
|
|
if not sack_sent:
|
|
session.send_cframe_sack()
|
|
sack_sent = True
|
|
|
|
# release lock for new packet to be sent
|
|
session.lock.release()
|
|
|
|
# WIP: The bNSeq, bNRcv, optional selective acknowledgment (SACK), and optional send mask fields are then processed by using the standard rules in sections 3.1.5.2.1 through 3.1.5.2.4
|
|
# TODO: A successfully validated SACK packet SHOULD count as a valid receive and thus restart the KeepAlive timer
|
|
|
|
if cframe.Command & CFrame.PACKET_COMMAND_POLL:
|
|
if not session:
|
|
logger.error("Received a POLL packet for a non fully connected session")
|
|
return
|
|
#logger.debug("Got a CFrame POLL. Replying with a SACK.")
|
|
# must send back a ACK
|
|
#session.send_cframe_sack()
|
|
except Exception:
|
|
logger.error("Should have been a CFRAME but could not parse it")
|
|
traceback.print_exc()
|
|
return
|
|
|
|
async def handle_new_dframe(self, addr, data):
|
|
try:
|
|
dframe = DFrame(data)
|
|
logger.debug("Received DFRAME")
|
|
session = next((x for x in self.addrs if x.ip == addr[0] and x.port == addr[1]), None)
|
|
if not session:
|
|
logger.debug("%s sent a DFRAME without having sent a CONNECT before. GTFO.", addr)
|
|
logger.debug(self.addrs)
|
|
return
|
|
if not dframe.Seq == session.next_expected_seq:
|
|
logger.error("%s unexpected SEQ. Got %s, expected %s", addr, dframe.Seq, session.next_expected_seq)
|
|
#return
|
|
|
|
if dframe.Control & DFrame.PACKET_CONTROL_END_STREAM:
|
|
if not session:
|
|
logger.error("Received a END STREAM packet for a non fully connected session")
|
|
return
|
|
# disconnect
|
|
resp = DFrame()
|
|
resp.Command = DFrame.PACKET_COMMAND_DATA | DFrame.PACKET_COMMAND_NEW_MSG | DFrame.PACKET_COMMAND_END_MSG | DFrame.PACKET_COMMAND_RELIABLE | DFrame.PACKET_COMMAND_SEQUENTIAL
|
|
resp.Control = DFrame.PACKET_CONTROL_END_STREAM
|
|
resp.Seq = dframe.NRcv
|
|
resp.NRcv = session.next_send
|
|
session.send(resp)
|
|
|
|
session.send_cframe_sack()
|
|
|
|
# TODO: broadcast session has disconnected
|
|
self.addrs.remove(session)
|
|
return
|
|
if dframe.Control & DFrame.PACKET_CONTROL_KEEPALIVE_OR_CORRELATE:
|
|
pass
|
|
#session.send_dframe_keepalive()
|
|
|
|
session.next_expected_seq += 1
|
|
if dframe.Payload:
|
|
await self.handle_game_packet(session, dframe.Payload)
|
|
else:
|
|
pass
|
|
except Exception as e:
|
|
logger.error("Could not parse DFRAME: ")
|
|
traceback.print_exc()
|
|
return
|
|
|
|
async def handle_game_packet(self, session, payload):
|
|
player = self.get_temp_player(session)
|
|
|
|
if payload[0] == 0xc1:
|
|
if not player or not player.phase == PlayerPhases.CFRAME_CONNECTED:
|
|
logger.error("Fuck you. Playerphase was %s", player.phase)
|
|
return
|
|
# DN_INTERNAL_MESSAGE_PLAYER_CONNECT_INFO_EX
|
|
ppayload = Packet(payload)
|
|
dwPacketType = ppayload.getULong()
|
|
dwFlags = ppayload.getULong()
|
|
dwDNETVersion = ppayload.getULong()
|
|
dwNameOffset = ppayload.getULong()
|
|
dwNameSize = ppayload.getULong()
|
|
dwDataOffset = ppayload.getULong()
|
|
dwDataSize = ppayload.getULong()
|
|
dwPasswordOffset = ppayload.getULong()
|
|
dwPasswordSize = ppayload.getULong()
|
|
dwConnectDataOffset = ppayload.getULong()
|
|
dwConnectDataSize = ppayload.getULong()
|
|
dwURLOffset = ppayload.getULong()
|
|
dwURLSize = ppayload.getULong()
|
|
guidInstance = payload[52:68]
|
|
guidApplication = payload[68:84]
|
|
name = payload[dwNameOffset+4:dwNameOffset+4+dwNameSize]
|
|
name = name.decode("utf-16-le")
|
|
logger.debug("%s connected!", name)
|
|
player.name = name
|
|
#session.send_dframe_keepalive()
|
|
|
|
# Response: DN_MSG_INTERNAL_SEND_CONNECT_INFO
|
|
d = DN_MSG_INTERNAl_SEND_CONNECT_INFO(self, player)
|
|
d.Seq = session.next_send
|
|
d.NRcv = session.next_expected_seq
|
|
|
|
appdata = Packet()
|
|
appdata.putByte(0xff) # map ID
|
|
appdata.putByte(self.server.game_type) # game type
|
|
appdata.putByte(self.server.teams) # teams
|
|
appdata.write(b'\x00') # original: 0x00 - does not seem to affect client
|
|
appdata.putShort(int(self.server.version * 1000))
|
|
appdata.write(b'\x03\x90') # original: 0292 - does not seem to affect client
|
|
appdata.putShort(self.server.points_per_capture)
|
|
appdata.putShort(self.server.points_per_kill)
|
|
appdata.write(b'\x00\x00') # original: 0000 - does not seem to affect client
|
|
appdata.putShort(self.server.detente_time)
|
|
appdata.write(b'\x9c\x53\xf4\xdd') # Seems to be a checksum of current map OR linked to the number of chars in the map name
|
|
appdata.write(self.server.currentmap.mapname.encode("ascii"))
|
|
appdata.write(b'\x00' * (32 - len(self.server.currentmap.mapname)))
|
|
d.ApplicationReservedData = appdata.getvalue()
|
|
session.send(d)
|
|
player.phase = PlayerPhases.DN_SEND_CONNECT_INFO
|
|
|
|
elif payload[0] == 0xc3:
|
|
player = self.get_temp_player(session)
|
|
if not player or not player.phase == PlayerPhases.DN_SEND_CONNECT_INFO:
|
|
return
|
|
|
|
player.phase = PlayerPhases.DN_ACK_CONNECT_INFO
|
|
await self.server.add_player(player)
|
|
#player.session.send_cframe_sack()
|
|
|
|
await player.session.send_gamedata(b'\x3c'+struct.pack("<L", player.id)+b"\x00")
|
|
|
|
p1 = b'\x01' + struct.pack("<B", len(self.server.players))
|
|
for pplayer in self.server.players:
|
|
p1 += struct.pack("<L", pplayer.id)
|
|
for pplayer in self.server.players:
|
|
if pplayer.name == "[Server]":
|
|
continue
|
|
p1 += b'\x0e'
|
|
p1 += b"\x01\x00"
|
|
for pplayer in self.server.players:
|
|
if pplayer == player or pplayer.name == "[Server]":
|
|
continue
|
|
p1 += struct.pack("<B", pplayer.team)
|
|
p1 += b'\x00\x00'
|
|
|
|
await player.session.send_gamedata(p1, acknow=False)
|
|
await player.session.send_gamedata(b'\x3d\x00\x5b\x53\x65\x72\x76\x65\x72\x5d'+b"\x00"*25) # [SERVER]
|
|
await player.session.send_gamedata(b'\x3d\x01'+player.name.encode("ascii")+b"\x00"*(33-len(player.name.encode("ascii")))) # playername
|
|
"""
|
|
player.session.send_gamedata(b'\x0f\x56\xab\x31\x96\x06\x00\x00\x00\x00\x00\x00\x00\x00') # unknown
|
|
player.session.send_gamedata(b'\x10\x02\x56\xab\x31\x96\x00\x00\x00\x00\x00\x00\x00\x00') # unknown
|
|
player.session.send_gamedata(b'\x29\x28\x00\x00\x80\x32\x00\x00\x80\x05\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00') # unknown
|
|
player.session.send_gamedata(b'\x39\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') # unknown
|
|
player.session.send_gamedata(b'\x39\x01\x02\x01\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') # unknown
|
|
player.session.send_gamedata(b'\x0f\x56\xab\x31\x96\x07\x00\x00\x00\x00\x00\x00\x00\x00') # unknown
|
|
|
|
player.session.send_gamedata(b'\x0a\x12\x00\x00\x00\x00\x28\x10\x03\x10\x12\x00\x00\x00\xc1\xff\x41\xff\x00', acknow=True) # unknown
|
|
player.session.send_gamedata(b'\x0f\x56\xab\x31\x96\x08\x00\x00\x00\x00\x00\x00\x00\x00', acknow=True) # unknown
|
|
player.session.send_gamedata(b'\x0a\x12\x00\x00\x00\x00\x2c\x10\x03\x10\x12\x00\x00\x00\x08\x00\xb6\x01\x0a\x14\x00\x00\x00\x00\x2d\x10\x03\x01\x12\x00\x00\x00\xd4\xfc\x9b\xfd\x00\x00\x00', acknow=True) # unknown
|
|
#\x00
|
|
player.session.send_gamedata(b'\x2f\x02\x00\x00\xbe\x00\x00\x00\x00\x00\x00\x00\x00\x00', acknow=True) # unknown
|
|
player.session.send_gamedata(b'\x0a\x12\x00\x00\x00\x00\x2e\x10\x03\x10\x12\x00\x00\x00\xa8\xfd\x2d\x03\x0a\x14\x00\x00\x00\x00\x2e\x10\x03\x01\x12\x00\x00\x00\x60\xfd\x14\x03\x00\x00\x00', acknow=True) # unknown
|
|
player.session.send_gamedata(b'\x0a\x14\x00\x00\x00\x00\x28\x10\x03\x01\x12\x00\x00\x00\xbf\xff\x3f\xff\x00\x00\x00', acknow=True) # unknown
|
|
player.session.send_gamedata(b'\x0a\x12\x00\x00\x00\x00\x2a\x10\x03\x10\x12\x00\x00\x00\x73\x01\x42\x02\x0a\x14\x00\x00\x00\x00\x2a\x10\x03\x01\x12\x00\x00\x00\xbe\x01\x8c\x02\x00\x00\x00', acknow=True) # unknown
|
|
player.session.send_gamedata(b'\x2f\x02\x00\x00\xc2\x00\x00\x00\x00\x00\x00\x00\x00\x00',acknow=True) # unknown
|
|
player.session.send_gamedata(b'\x0a\x14\x00\x00\x00\x00\x2c\x10\x03\x01\x12\x00\x00\x00\x06\x00\xb5\x01\x00\x00\x00', acknow=True) # unknown
|
|
player.session.send_gamedata(b'\x0a\x12\x00\x00\x00\x00\x2e\x10\x03\x10\x12\x00\x00\x00\xa4\xfd\x2c\x03\x00',acknow=True) # unknown
|
|
player.session.send_gamedata(b'\x2f\x02\x00\x00\xc2\x00\x00\x00\x00\x00\x00\x00\x00\x00', acknow=True) # unknown
|
|
"""
|
|
else:
|
|
logger.debug("OPCODE: %s VALUES: %s", payload[0], payload[1:])
|
|
|
|
|
|
def send_packet(self, addr, packet):
|
|
self.remotesocket.sendto(packet.getvalue(), addr)
|
|
logger.debug("%s:%s < %s", addr[0], addr[1], packet.getvalue().hex())
|
|
|
|
def get_session(self, iporaddr, port=None):
|
|
if not port:
|
|
return next((x for x in self.addrs if x.ip == iporaddr[0] and x.port == iporaddr[1]), None)
|
|
else:
|
|
return next((x for x in self.addrs if x.ip == iporaddr and x.port == port), None)
|
|
|
|
def get_player(self, iporaddr, port=None):
|
|
if type(iporaddr) == Session:
|
|
a = next((x for x in self.server.players if x.session.ip == iporaddr.ip and x.session.port == iporaddr.port), None)
|
|
if a:
|
|
return a
|
|
else:
|
|
return next((x for x in self.server.tempplayers if x.session.ip == iporaddr.ip and x.session.port == iporaddr.port),None)
|
|
if not port:
|
|
a = next((x for x in self.server.players if x.session.ip == iporaddr[0] and x.session.port == iporaddr[1]), None)
|
|
if a:
|
|
return a
|
|
else:
|
|
return next((x for x in self.server.tempplayers if x.session.ip == iporaddr[0] and x.session.port == iporaddr[1]),None)
|
|
else:
|
|
a = next((x for x in self.server.players if x.session.ip == iporaddr and x.session.port == port), None)
|
|
if a:
|
|
return a
|
|
else:
|
|
return next((x for x in self.server.tempplayers if x.session.ip == iporaddr and x.session.port == port), None)
|
|
|
|
def get_temp_player(self, iporaddr, port=None):
|
|
if type(iporaddr) == Session:
|
|
return next((x for x in self.server.tempplayers if x.session.ip == iporaddr.ip and x.session.port == iporaddr.port), None)
|
|
if not port:
|
|
return next((x for x in self.server.tempplayers if x.session.ip == iporaddr[0] and x.session.port == iporaddr[1]), None)
|
|
else:
|
|
return next((x for x in self.server.tempplayers if x.session.ip == iporaddr and x.session.port == port), None) |