306 lines
14 KiB
Python
306 lines
14 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
|
||
|
from utils.logger import setup_logger
|
||
|
from giants.player import Player
|
||
|
|
||
|
logger = setup_logger(__name__)
|
||
|
|
||
|
|
||
|
class Netserver:
|
||
|
def __init__(self, server):
|
||
|
self.socket = None
|
||
|
self.statssocket = None
|
||
|
self.server = server
|
||
|
self.addrs = []
|
||
|
self.guid = uuid.uuid4().bytes
|
||
|
|
||
|
def run(self):
|
||
|
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)
|
||
|
gameserverthread = threading.Thread(target=self.handle_packets)
|
||
|
gameserverthread.start()
|
||
|
|
||
|
#ms = MasterServer(self.server)
|
||
|
#statsserverthread = threading.Thread(target=ms.register_and_run)
|
||
|
#statsserverthread.start()
|
||
|
|
||
|
gameserverthread.join()
|
||
|
#statsserverthread.join()
|
||
|
|
||
|
def handle_packets(self):
|
||
|
while True:
|
||
|
data, addr = self.socket.recvfrom(1024)
|
||
|
logger.debug("%s:%s > %s", addr[0], addr[1], data.hex())
|
||
|
self.handle_new_packet(addr, data)
|
||
|
|
||
|
def handle_new_packet(self, addr, bData):
|
||
|
pPacket = Packet(bData)
|
||
|
first = pPacket.getByte()
|
||
|
if len(bData) >= 4 and first & DFrame.PACKET_COMMAND_DATA:
|
||
|
self.handle_new_dframe(addr, pPacket)
|
||
|
elif len(bData) >= 12 and first & CFrame.PACKET_COMMAND_FRAME:
|
||
|
self.handle_new_cframe(addr, pPacket)
|
||
|
elif first == EnumQuery.LEAD or first == EnumResponse.LEAD:
|
||
|
self.handle_new_enum(addr, pPacket)
|
||
|
else:
|
||
|
logger.error("Unknown frame received")
|
||
|
return
|
||
|
|
||
|
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'
|
||
|
er.ApplicationReservedData += b'\xd9\x05' # 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")
|
||
|
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)
|
||
|
|
||
|
def handle_new_cframe(self, addr, data):
|
||
|
try:
|
||
|
cframe = CFrame(data)
|
||
|
session = self.get_session(addr)
|
||
|
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.socket)
|
||
|
session.SessID = cframe.SessID
|
||
|
session.ip = addr[0]
|
||
|
session.port = addr[1]
|
||
|
self.addrs.append(session)
|
||
|
session.send_cframe_connected(cframe)
|
||
|
#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()
|
||
|
|
||
|
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.CFRAME_NRcv == 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
|
||
|
|
||
|
# 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
|
||
|
|
||
|
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
|
||
|
|
||
|
session.next_expected_seq += 1
|
||
|
if dframe.Payload:
|
||
|
self.handle_game_packet(session, dframe.Payload)
|
||
|
else:
|
||
|
pass
|
||
|
except Exception as e:
|
||
|
logger.error("Could not parse DFRAME: ")
|
||
|
traceback.print_exc()
|
||
|
return
|
||
|
|
||
|
def handle_game_packet(self, session, payload):
|
||
|
logger.debug("Payload: %s", payload)
|
||
|
if payload[0] == 0xc1:
|
||
|
# connect frame?
|
||
|
_ = payload[0:4]
|
||
|
_ = payload[4:8]
|
||
|
_ = payload[8:12]
|
||
|
_ = payload[12:16]
|
||
|
_ = payload[16:20]
|
||
|
_ = payload[20:52]
|
||
|
instanceguid = payload[52:68]
|
||
|
gameguid = payload[68:84]
|
||
|
_ = payload[84:88]
|
||
|
_ = payload[88:92]
|
||
|
name = payload[92:]
|
||
|
name = name.decode("utf-16-le")[0:-1]
|
||
|
logger.debug("%s connected!", name)
|
||
|
player = Player(name, session)
|
||
|
self.server.new_player(player)
|
||
|
session.send_dframe_keepalive()
|
||
|
|
||
|
r = DFrame()
|
||
|
r.Command = DFrame.PACKET_COMMAND_DATA | DFrame.PACKET_COMMAND_RELIABLE | DFrame.PACKET_COMMAND_SEQUENTIAL | DFrame.PACKET_COMMAND_POLL | DFrame.PACKET_COMMAND_NEW_MSG | DFrame.PACKET_COMMAND_END_MSG | DFrame.PACKET_COMMAND_USER_1
|
||
|
r.Control = 0x00
|
||
|
r.Seq = session.next_send
|
||
|
r.NRcv = session.next_expected_seq
|
||
|
payload = Packet()
|
||
|
payload.putLong(0xc2) # C2 = response to a C1 connection?
|
||
|
payload.putLong(0x00) # unknown
|
||
|
payload.putLong(0x00) # unknown
|
||
|
payload.putLong(0x50) # unknown - 80
|
||
|
payload.putLong(0x41) # unknown - 65
|
||
|
payload.putLong(0x14) # max players
|
||
|
payload.putLong(0x02) # current players
|
||
|
payload.write(b'\x0e\x01\x00\x00') # unknown - 3585 - or \x16\x01\x00\x00
|
||
|
payload.putLong(len((self.server.name + "\x00").encode("utf-16-le"))) # SERVERNAME LENGTH
|
||
|
for _ in range(4):
|
||
|
payload.putLong(0) # unknown
|
||
|
payload.putLong(0xda) # unknown - 218 - or 0xe2
|
||
|
payload.putLong(0x34) # unknown - 52
|
||
|
payload.write(self.guid) # instance guid
|
||
|
payload.write(
|
||
|
b"\x10\x5e\x62\xa7\x96\x1a\xd2\x11\x9a\xfc\x00\x60\x08\x45\xe5\x71") # app guid
|
||
|
payload.write(b'\x40\xf5\x62\xe1')
|
||
|
payload.putLong(0x21) # unknown - 33
|
||
|
payload.putLong(0x00) # unknown
|
||
|
payload.putLong(0x02) # unknown - 2
|
||
|
payload.putLong(0x00)
|
||
|
payload.write(b'\x45\xf5\x52\xe3')
|
||
|
payload.putLong(0x00)
|
||
|
payload.write(b'\x02\x04\x00\x00') # unknown
|
||
|
payload.putLong(0x02) # unknown - 2
|
||
|
payload.putLong(0x00) # unknown
|
||
|
payload.putLong(0x07) # unknown - 7
|
||
|
for _ in range(6):
|
||
|
payload.putLong(0) # unknown
|
||
|
payload.write(b'\x40\xf5\x62\xe1') # unknown - 1089823457
|
||
|
payload.putLong(0x00) # unknown
|
||
|
payload.write(b'\x00\x02\x00\x00') # unknown - ?
|
||
|
payload.putLong(0x21) # unknown - 33
|
||
|
payload.putLong(0x00) # unknown - 0
|
||
|
payload.putLong(0x07) # unknown - 7
|
||
|
payload.putLong(0xcc) # unknown - 204
|
||
|
payload.putLong(0x0e) # unknown - 6
|
||
|
for _ in range(4):
|
||
|
payload.putLong(0) # unknown
|
||
|
payload.write((name + "\x00").encode("utf-16-le"))
|
||
|
payload.write(b'\xff') # map ID
|
||
|
payload.write(b'\x01\x01\x00')
|
||
|
#payload.write(b'\x00\x04\x00')
|
||
|
payload.write(b'\xd9\x05') # game version
|
||
|
payload.write(b'\x02\x92\x05\x00\x01\x00\x00\x00\x00\x00') # Unknown
|
||
|
payload.write(b'\x9c\x53\xf4\xde') # Seems to be a checksum of current map
|
||
|
payload.write(self.server.currentmap.mapname.encode("ascii"))
|
||
|
payload.putLong(0x00) # ??
|
||
|
payload.write((self.server.name + "\x00").encode("utf-16-le"))
|
||
|
|
||
|
r.Payload = payload.getvalue()
|
||
|
session.send(r)
|
||
|
|
||
|
if payload[0] == 0xc3:
|
||
|
self.server.broadcast_message(name + " is a dick.")
|
||
|
|
||
|
|
||
|
def send_packet(self, addr, packet):
|
||
|
self.socket.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)
|