Compare commits

..

14 Commits

28 changed files with 1308 additions and 85 deletions

1
.gitignore vendored
View File

@@ -52,7 +52,6 @@ coverage.xml
*.pot *.pot
# Django stuff: # Django stuff:
*.log
local_settings.py local_settings.py
# Flask stuff: # Flask stuff:

View File

@@ -3,7 +3,6 @@
"logging": { "logging": {
"filename": "", "filename": "",
"level": "DEBUG", "level": "DEBUG",
"format": "[%(name)s:L%(lineno)02d %(levelname)s] %(message)s",
"datefmt": "%d-%b-%y %H:%M:%S" "datefmt": "%d-%b-%y %H:%M:%S"
} }
}, },

123
notes/dumps/1586009002.log Normal file
View File

@@ -0,0 +1,123 @@
----------
Loaded log file: "server_log.txt".
----------
SA-MP Dedicated Server
----------------------
v0.3.7-R2, (C)2005-2015 SA-MP Team
Server Plugins
--------------
Loading plugin: SAMPExtender
Loaded.
Loaded 1 plugins.
SAMP Extender
-------------
Callback hooks:
[x] 0x0045F7E0 -> 0x7A4F119A: CodeTrace::Run(0x0045f7e0)
[x] 0x0045C000 -> 0x7A4F1118: CodeTrace::Run(0x0045c000)
[x] 0x0044DA70 -> 0x7A4F1276: CodeTrace::Run(0x0044da70)
[x] 0x0044DB30 -> 0x7A4F140B: CodeTrace::Run(0x0044db30)
[x] 0x0044DD00 -> 0x7A4F1410: CodeTrace::Run(0x0044dd00)
[x] 0x0044DED0 -> 0x7A4F1415: CodeTrace::Run(0x0044ded0)
Installed: 6
Failed: 0
Ban list
--------
Loaded: samp.ban
Started server on port: 7777, with maxplayers: 50 lanmode is OFF.
Filterscripts
---------------
Loaded 0 filterscripts.
Number of vehicle models: 1
[connection] 127.0.0.1:64555 requests connection cookie.
[connection] incoming connection: 127.0.0.1:64555 id: 0
HandleSocketReceiveFromConnectedPlayer()
caller_addr: 0x00456C9C
length: 22
MTUSize: 576
Player port: 64555
buffer:
00 00 42 90 0b 61 61 61 61 62 62 62 62 63 63 63 63 64 64 64 64 00
CreateInternalPacketFromBitStream()
caller_addr: 0x0045FB0D
time: 21 -905708544
bitStream->numberOfBitsAllocated: 176
bitStream->numberOfBitsUsed: 176
bitStream->readOffset: 1
bitStream->data:
00 00 42 90 0b 61 61 61 61 62 62 62 62 63 63 63 63 64 64 64 64 00
bitStream->data(bits):
0|0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 1 0 0 1 0 0 0 0 0 0 0 0 1 0 1 1 0 1 1 0 0 0 0 1
0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0
0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 1 0 0
0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0
BitStream::ReadBits()
caller_addr: 0x0045C046
alignBitsToRight: 1
numberOfBitsToRead: 16
bitstream->read(bits):
0[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]1 0 0 0 0 1 0 1 0 0 1 0 0 0 0 0 0 0 0 1 0 1 1 0 1 1 0 0 0 0 1
0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0
0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 1 0 0
0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0
BitStream::ReadBits()
caller_addr: 0x0045C063
alignBitsToRight: 1
numberOfBitsToRead: 4
bitstream->read(bits):
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0[1 0 0 0]0 1 0 1 0 0 1 0 0 0 0 0 0 0 0 1 0 1 1 0 1 1 0 0 0 0 1
0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0
0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 1 0 0
0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0
BitStream::ReadCompressed()
caller_addr: 0x0045C0FF
size: 16
unsignedData: 1
BitStream::ReadBits()
caller_addr: 0x0044DC21
alignBitsToRight: 1
numberOfBitsToRead: 8
bitstream->read(bits):
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 [1 0 0 1 0 0 0 0] 0 0 0 0 1 0 1 1 0 1 1 0 0 0 0 1
0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0
0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 1 0 0
0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0
BitStream::ReadAlignedBytes()
caller_addr: 0x0045C14A
numberOfBytesToRead: 18
bitstream->read(bits):
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 1 0 0 1 0 0 0 0 [0 0 0 0 1 0 1 1 0 1 1 0 0 0 0 1
0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0
0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 1 0 0
0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0
CreateInternalPacketFromBitStream()
caller_addr: 0x00460130
time: 21 -905708544
bitStream->numberOfBitsAllocated: 176
bitStream->numberOfBitsUsed: 176
bitStream->readOffset: 176
bitStream->data:
00 00 42 90 0b 61 61 61 61 62 62 62 62 63 63 63 63 64 64 64 64 00
bitStream->data(bits):
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 1 0 0 1 0 0 0 0 0 0 0 0 1 0 1 1 0 1 1 0 0 0 0 1
0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0
0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 1 0 0
0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0

148
notes/dumps/1586009798.log Normal file
View File

@@ -0,0 +1,148 @@
----------
Loaded log file: "server_log.txt".
----------
SA-MP Dedicated Server
----------------------
v0.3.7-R2, (C)2005-2015 SA-MP Team
Server Plugins
--------------
Loading plugin: SAMPExtender
Loaded.
Loaded 1 plugins.
SAMP Extender
-------------
Callback hooks:
[x] 0x0045F7E0 -> 0x7A53119A: CodeTrace::Run(0x0045f7e0)
[x] 0x0045C000 -> 0x7A531118: CodeTrace::Run(0x0045c000)
[x] 0x0044DA70 -> 0x7A531276: CodeTrace::Run(0x0044da70)
[x] 0x0044DB30 -> 0x7A53140B: CodeTrace::Run(0x0044db30)
[x] 0x0044DD00 -> 0x7A531410: CodeTrace::Run(0x0044dd00)
[x] 0x0044DED0 -> 0x7A531415: CodeTrace::Run(0x0044ded0)
[x] 0x0044D840 -> 0x7A53141A: CodeTrace::Run(0x0044d840)
Installed: 7
Failed: 0
Ban list
--------
Loaded: samp.ban
Started server on port: 7777, with maxplayers: 50 lanmode is OFF.
Filterscripts
---------------
Loaded 0 filterscripts.
Number of vehicle models: 1
[connection] 127.0.0.1:56092 requests connection cookie.
[connection] incoming connection: 127.0.0.1:56092 id: 0
HandleSocketReceiveFromConnectedPlayer()
caller_addr: 0x00456C9C
length: 22
MTUSize: 576
Player port: 56092
buffer:
00 00 42 90 0b 61 61 61 61 62 62 62 62 63 63 63 63 64 64 64 64 00
BitStream::ReadBit() # hasAcks
caller_addr: 0x0045F93C
bitstream->read(bits):
[0]0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 1 0 0 1 0 0 0 0 0 0 0 0 1 0 1 1 0 1 1 0 0 0 0 1
0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0
0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 1 0 0
0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0
CreateInternalPacketFromBitStream()
caller_addr: 0x0045FB0D
time: 22 39423557
bitStream->numberOfBitsAllocated: 176 # 22 bytes
bitStream->numberOfBitsUsed: 176
bitStream->readOffset: 1
bitStream->data:
00 00 42 90 0b 61 61 61 61 62 62 62 62 63 63 63 63 64 64 64 64 00
bitStream->data(bits):
0|0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 1 0 0 1 0 0 0 0 0 0 0 0 1 0 1 1 0 1 1 0 0 0 0 1
0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0
0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 1 0 0
0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0
BitStream::ReadBits() # messageNumber
caller_addr: 0x0045C046
alignBitsToRight: 1
numberOfBitsToRead: 16
bitstream->read(bits):
0[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]1 0 0 0 0 1 0 1 0 0 1 0 0 0 0 0 0 0 0 1 0 1 1 0 1 1 0 0 0 0 1
0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0
0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 1 0 0
0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0
BitStream::ReadBits() # reliability
caller_addr: 0x0045C063
alignBitsToRight: 1
numberOfBitsToRead: 4
bitstream->read(bits):
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0[1 0 0 0]0 1 0 1 0 0 1 0 0 0 0 0 0 0 0 1 0 1 1 0 1 1 0 0 0 0 1
0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0
0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 1 0 0
0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0
BitStream::ReadBit() # isSplitPacket
caller_addr: 0x0045C0BC
bitstream->read(bits):
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0[0]1 0 1 0 0 1 0 0 0 0 0 0 0 0 1 0 1 1 0 1 1 0 0 0 0 1
0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0
0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 1 0 0
0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0
BitStream::ReadCompressed() # unsure how this works
caller_addr: 0x0045C0FF
size: 16
unsignedData: 1
BitStream::ReadBit() # inside ReadCompressed
caller_addr: 0x0044DBC6
bitstream->read(bits):
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1[0] 1 0 0 1 0 0 0 0 0 0 0 0 1 0 1 1 0 1 1 0 0 0 0 1
0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0
0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 1 0 0
0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0
BitStream::ReadBits() # inside ReadCompressed
caller_addr: 0x0044DC21
alignBitsToRight: 1
numberOfBitsToRead: 8
bitstream->read(bits):
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 [1 0 0 1 0 0 0 0] 0 0 0 0 1 0 1 1 0 1 1 0 0 0 0 1
0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0
0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 1 0 0
0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0
BitStream::ReadAlignedBytes() # the actual data (back inside CreateInternalPacketFromBitStream now)
caller_addr: 0x0045C14A
numberOfBytesToRead: 18
bitstream->read(bits):
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 1 0 0 1 0 0 0 0 [0 0 0 0 1 0 1 1 0 1 1 0 0 0 0 1
0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0
0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 1 0 0
0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0
CreateInternalPacketFromBitStream()
caller_addr: 0x00460130
time: 22 39423557
bitStream->numberOfBitsAllocated: 176
bitStream->numberOfBitsUsed: 176
bitStream->readOffset: 176
bitStream->data:
00 00 42 90 0b 61 61 61 61 62 62 62 62 63 63 63 63 64 64 64 64 00
bitStream->data(bits):
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 1 0 0 1 0 0 0 0 0 0 0 0 1 0 1 1 0 1 1 0 0 0 0 1
0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0
0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 1 0 0
0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0

45
notes/notes.txt Normal file
View File

@@ -0,0 +1,45 @@
SendImmediate( (char*)bitStream.GetData(), bitStream.GetNumberOfBitsUsed(), SYSTEM_PRIORITY, UNRELIABLE, 0, target, false, false, currentTimeNS )
char* data = bitStream.GetData()
int numberOfBitsToSend = bitStream.GetNumberOfBitsUsed()
PacketPriority priority = SYSTEM_PRIORITY
PacketReliability reliability = UNRELIABLE
char orderingChannel = 0
PlayerID playerId = target
bool broadcast = false
bool useCallerDataAllocation = false
RakNetTimeNS currentTime = currentTimeNS
Send( char *data, int numberOfBitsToSend, PacketPriority priority, PacketReliability reliability, unsigned char orderingChannel, bool makeDataCopy, int MTUSize, RakNetTimeNS currentTime )
char* data = bitStream.GetData()
int numberOfBitsToSend = bitStream.GetNumberOfBitsUsed()
PacketPriority priority = SYSTEM_PRIORITY
PacketReliability reliability = UNRELIABLE
char orderingChannel = 0
bool makeDataCopy = true
int MTUSize = 576
RakNetTimeNS currentTime = currentTimeNS
internalPacket->creationTime = currentTime
internalPacket->data = data // (cpy)
internalPacket->dataBitLength = numberOfBitsToSend
internalPacket->nextActionTime = 0
internalPacket->messageNumber = messageNumber
internalPacket->priority = priority
internalPacket->reliability = reliability
internalPacket->splitPacketCount = 0
int headerLength = BITS_TO_BYTES( GetBitStreamHeaderLength(internalPacket) )
int maxDataSize = 576 - 28 - headerLength // MTUSize - UDP_HEADER_SIZE - headerLength
; More stuff to note from
...
sendPacketSet[ internalPacket->priority ].Push( internalPacket );

44
notes/packet_writeup.md Normal file
View File

@@ -0,0 +1,44 @@
# General packet
## Client to server spesific
* Encode (encrypt) data
* use first byte as checksum
* every byte ^ 0xAA added together
* xor
* Server port ^ 0xCCCC
* Every other byte
* Lookup table (use byte as indexing value)
## Shared
* Can be internal packet
* If packet is smaller then 3 bytes; forward to plugin handlers (*)before handling them (len(data) <= 2)
## Order of operations
1. Check if banned (if true, cancel rest and handle) [^1]
2. If first byte in data is (0x19, 0x1d, 0x1f, 0x24 or 0x18) custom code flow required [^2]
3. HandleSocketReceiveFromConnectedPlayer (cancel rest if handled)
4. Check if cheat (modified) packet (unexpected at this time, there are some packets we can expect) [^3]
[^1]: this could be changed in custom implementation (dont allow inital connection instead)
[^2]: if (0x18 and length == 2) or (0x19 and length <= 2):
handle packet and return
elif (0x1d or 0x1f or 0x24) and length <= 2:
handle packet, but continue
[^3]: if ((0x18 or 0x1a) and length <= 3) or ((0x19 or 0x1d) and length <= 2) or
((0x08 or 0x07 or 0x27) and length >= 5) or (0x37 and length < 400):
We expect this packet, everything is ok?
else:
This is a cheat (modified / injected) packet that should not arrive at this time
Send a packet to client that you received a modified packet (0x26)
# Internal packet
|condition|name|bit length|type|notes|
|---|---|---|---|---|
||messageNumber|0x10|ushort||
||reliability|0x04|uchar|Has to be over 5 (value > 5)|
|reliability in (7, 10, 9)|orderingChannel|0x05|||
|reliability in (7, 10, 9)|orderingIndex|0x10|ushort||
||isSplitPacket|0x01|bool|Drop packet as we no longer support split packet?|
|isSplitPacket == 0|dataBitLength|0x10|ushort||
|isSplitPacket == 0|data|dataBitLength|uchar*||

12
notes/packetlist.txt Normal file
View File

@@ -0,0 +1,12 @@
0x07 ID_PING
0x08 ID_PING_OPEN_CONNECTIONS
0x0B ID_CONNECTION_REQUEST
0x18 ID_OPEN_CONNECTION_REQUEST
0x19 ID_OPEN_CONNECTION_REPLY
0x1D ID_CONNECTION_ATTEMPT_FAILED
0x1F ID_NO_FREE_INCOMING_CONNECTIONS
0x24 ID_CONNECTION_BANNED
0x25 ID_INVALID_PASSWORD
0x26 ID_MODIFIED_PACKET
0x27 ID_PONG
0x37 ID_ADVERTISE_SYSTEM

View File

@@ -0,0 +1,3 @@
0x07 UNRELIABLE_SEQUENCED
0x0A RELIABLE_SEQUENCED
0x09 RELIABLE_ORDERED

View File

@@ -1,7 +1,10 @@
from . import env from . import helpers
from . import server from . import server
from . import client from . import client
from . import shared from . import shared
from . import struct from . import struct
from . import client
from . import env
#from . import logging

View File

@@ -1,52 +1,6 @@
import struct
import socket
import asyncio
from time import time
from . import base from . import base
from . import query from . import query
from . import player from . import player
from . import client
STATE_UNKNOWN = (0, base.BaseClient) from .client import Client
STATE_QUERY = (1, query.QueryClient)
STATE_PLAYER = (2, player.PlayerClient)
TIMEOUT = 10 # assume connection is closed after 10 seconds of inactivity (change this to a higher value so you dont timeout while debugging might be a good idea)
class Client:
def __init__(self, server: "__ServerInstance__", ip: str, port: int):
self.server = server
self.ip = ip
self.port = port
self.set_state(STATE_UNKNOWN)
self.last_active = time()
self.keep_alive_task = asyncio.create_task( self.keep_alive() )
self.connected = True # keep_alive will set this to False if connection has not been interacted with for a while (allowing server loop to remove their reference)
def set_state(self, state: tuple):
#self.keep_alive_task.cancel()
self.state = state
self.client = self.state[1](self.server, self.ip, self.port)
async def on_packet(self, packet: bytes):
self.last_active = time()
if self.state == STATE_UNKNOWN:
# We are currently unaware if this is a player client or query client, but we got a packet that will be our check to know
if packet.startswith(b"SAMP"):
self.set_state(STATE_QUERY)
else:
self.set_state(STATE_PLAYER)
await self.client.on_packet(packet)
async def keep_alive(self): # Maybe bad name for this method as it rather checks if connection is dropped
while True:
timestamp = time()
if self.last_active + TIMEOUT - timestamp < 0:
self.connected = False
return
await asyncio.sleep(self.last_active + TIMEOUT - timestamp)

View File

@@ -4,6 +4,7 @@ import struct
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BaseClient: class BaseClient:
def __init__(self, server: "__ServerInstance__", ip: str, port: int): def __init__(self, server: "__ServerInstance__", ip: str, port: int):
self.server = server self.server = server

53
sampy/client/client.py Normal file
View File

@@ -0,0 +1,53 @@
import struct
import socket
import asyncio
from time import time
from . import base
from . import query
from . import player
STATE_UNKNOWN = (0, base.BaseClient)
STATE_QUERY = (1, query.QueryClient)
STATE_PLAYER = (2, player.PlayerClient)
TIMEOUT = 10 # assume connection is closed after 10 seconds of inactivity (change this to a higher value so you dont timeout while debugging might be a good idea)
class Client:
def __init__(self, server: "__ServerInstance__", ip: str, port: int):
self.server = server
self.ip = ip
self.port = port
self.set_state(STATE_UNKNOWN)
self.last_active = time()
self.keep_alive_task = asyncio.create_task(self.keep_alive())
self.connected = True # keep_alive will set this to False if connection has not been interacted with for a while (allowing server loop to remove their reference)
def set_state(self, state: tuple):
# self.keep_alive_task.cancel()
self.state = state
self.client = self.state[1](self.server, self.ip, self.port)
async def on_packet(self, packet: bytes):
self.last_active = time()
if self.state == STATE_UNKNOWN:
# We are currently unaware if this is a player client or query client, but we got a packet that will be our check to know
if packet.startswith(b"SAMP"):
self.set_state(STATE_QUERY)
else:
self.set_state(STATE_PLAYER)
await self.client.on_packet(packet)
async def keep_alive(self): # Maybe bad name for this method as it rather checks if connection is dropped
while True:
timestamp = time()
if self.last_active + TIMEOUT - timestamp < 0:
self.connected = False
return
await asyncio.sleep(self.last_active + TIMEOUT - timestamp)

View File

@@ -1,14 +1,190 @@
import socket import socket
import struct
from time import time_ns
from .base import BaseClient from .base import BaseClient
from ..raknet.bitstream import Bitstream
import logging from ..helpers import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
STATE_CONNECTING = 0
STATE_CONNECTED = 1
class PlayerClient(BaseClient): class PlayerClient(BaseClient):
ranges = []
resend_list = dict()
def __init__(self, server: "__ServerInstance__", ip: str, port: int): def __init__(self, server: "__ServerInstance__", ip: str, port: int):
super().__init__(server, ip, port) super().__init__(server, ip, port)
logger.debug("Client resolved to PlayerClient") logger.debug("Client resolved to PlayerClient")
# TODO: check if banned and handle here
self.state = STATE_CONNECTING
# TODO: Make this static
self.handlers = {
"default": {
"default": self.on_unimplemented_state
},
STATE_CONNECTING: {
"default": self.on_unexpected,
0x18: self.on_connection_request
},
STATE_CONNECTED: {
"default": self.on_player_packet
}
}
async def on_packet(self, packet: bytes): async def on_packet(self, packet: bytes):
logger.debug("on_packet(%s)" % packet) logger.debug("on_packet(%s)" % packet)
#try:
packet = self.server.compressor.decompress(packet)
current_handlers = self.handlers.get(self.state, self.handlers["default"])
await current_handlers.get(packet[0], current_handlers["default"])(packet)
#except Exception as err:
#logger.error(err)
async def on_unexpected(self, packet: bytes):
logger.warning("Received unexpected packet: %s" % packet)
async def on_unimplemented_state(self, packet: bytes):
logger.warning("Unimplemented state: %d" % self.state)
async def on_unimplemented(self, packet: bytes):
logger.warning("Unimplemented handler for packet: %s" % packet)
async def on_connection_request(self, packet: bytes):
logger.debug("on_connection_request(%s)" % packet)
challenge = (self.ip_uint ^ self.server.config.challenge_short) & 0xFFFF
challenge_solution = challenge ^ 0x6969 # 0x6969 being client_version?
challenge_answer, = struct.unpack_from(b"<H", packet, 1)
if challenge_answer != challenge_solution: # Did not pass challenge
logger.debug("challenge: failed")
await self.send(struct.pack(b"<BH", 0x1A, challenge))
return
logger.debug("challenge: passed")
self.state = STATE_CONNECTED
await self.send(b"\x19\x00") # Challenge passed
async def on_player_packet(self, packet: bytes):
bitstream = Bitstream(packet)
time = time_ns()
_, has_acks = bitstream.read_bit()
if has_acks:
#logger.debug("Unfinished code hit; has_acks: True")
await self.handle_acknowledgement(bitstream, time)
#return
handled = await self.handle_internal_packet(bitstream)
if not handled:
logger.debug("Internal packet were not handled")
async def handle_acknowledgement(self, bitstream: Bitstream, time: int) -> bool:
if not self.deserialize(bitstream):
return False
for i in range(len(self.ranges)):
min, max = self.ranges[i][0], self.ranges[i][1]
if min > max:
return False
for i in range(min, max + 1, 1):
acked_histogram_counter = self.remove_packet_from_resend_list_and_delete_older_reliable_sequence()
def deserialize(self, bitstream: Bitstream): # TODO: Something weird with range thing DS_RangeList.h@L92
self.ranges.clear()
success, count = bitstream.read_compressed(0x10, True)
if not success:
return False
count, = struct.unpack(b"<H", count)
min, max = 0, 0
for i in range(count):
_, max_equal_to_min = bitstream.read_bit()
success, min = bitstream.read_bits(0x10)
if not success:
return False
min, = struct.unpack(b"<H", min)
if not max_equal_to_min:
success, max = bitstream.read_bits(0x10)
if not success:
return False
max, = struct.unpack(b"<H", max)
if max < min:
return False
else:
max = min
self.ranges.append((min, max))
return True
def remove_packet_from_resend_list_and_delete_older_reliable_sequence(self, message_number: int, time: int):
return 0 # TODO
async def handle_internal_packet(self, bitstream: Bitstream) -> bool:
if bitstream.length - bitstream.offset < 0x10:
return False
_, message_number = bitstream.read_bits(0x10)
_, reliability = bitstream.read_bits(0x04)
if reliability in (7, 9, 10):
success, ordering_channel = bitstream.read_bits(0x05)
if not success:
return False
success, ordering_index = bitstream.read_bits(0x10)
if not success:
return False
_, is_split_packet = bitstream.read_bit()
if is_split_packet:
logger.warning("Skipping split packet")
return
# TODO: set global split_packet index and count back to 0
# Something I dont understand yet
# TODO: ReadCompressed
#bitstream.offset += 1
success, data_bit_length = bitstream.read_compressed(0x10, True)
if not success:
return False
#_, unknown = bitstream.read(0x01)
#_, bit_length = bitstream.read(0x08)
##
#logger.debug("bit_length: %d" % bit_length)
#logger.debug("bitstream: %s" % bitstream)
data_bit_length, = struct.unpack(b"<H", data_bit_length)
# TODO: ReadAlignedBytes
#_, data = bitstream.read(bit_length)
_, data = bitstream.read_aligned_bytes((data_bit_length + 7) >> 3)
logger.debug("data: %s" % data)
return await self.handle_packet(data)
async def handle_packet(self, packet: bytes):
if packet[0] == 0x0B: # ID_CONNECTION_REQUEST
pass # TODO: Send ack and data after
return False

View File

@@ -7,6 +7,7 @@ from ..shared import glob
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class QueryClient(BaseClient): class QueryClient(BaseClient):
def __init__(self, server: "__ServerInstance__", ip: str, port: int): def __init__(self, server: "__ServerInstance__", ip: str, port: int):
super().__init__(server, ip, port) super().__init__(server, ip, port)
@@ -96,7 +97,7 @@ class QueryClient(BaseClient):
p["ping"] p["ping"]
] ]
return packet + struct.pack(b"<H" + (b"cc%dsII" * data[0]) % tuple(len(p["nick"]) for p in players), *data) return packet + struct.pack(b"<H" + (b"BB%dsII" * data[0]) % tuple(len(p["nick"]) for p in players), *data)
async def query_p(self, packet: bytes) -> bytes: async def query_p(self, packet: bytes) -> bytes:
return packet return packet

View File

@@ -3,7 +3,6 @@
"logging": { "logging": {
"filename": "", "filename": "",
"level": "DEBUG", "level": "DEBUG",
"format": "[%(name)s:L%(lineno)02d %(levelname)s] %(message)s",
"datefmt": "%d-%b-%y %H:%M:%S" "datefmt": "%d-%b-%y %H:%M:%S"
} }
}, },

View File

@@ -4,6 +4,7 @@ from threading import Thread
from .struct.server import ServerConfig from .struct.server import ServerConfig
from .server import Server from .server import Server
class Environment(Thread): class Environment(Thread):
def __init__(self, config: ServerConfig): def __init__(self, config: ServerConfig):
super().__init__() super().__init__()

View File

@@ -0,0 +1 @@
from . import logging

62
sampy/helpers/logging.py Normal file
View File

@@ -0,0 +1,62 @@
import sys
import logging
from logging import *
if sys.platform == 'win32':
import colorama
colorama.init()
FORMAT_STR_NORMAL = "%(levelname)s \x1b[33;1m%(asctime)s.%(msecs)03d \x1b[30;1m|\x1b[0m %(message)s"
COLOR_RESET = 0
COLOR_BLACK = 30
COLOR_RED = 31
COLOR_GREEN = 32
COLOR_YELLOW = 33
COLOR_BLUE = 34
COLOR_PURPLE = 35
COLOR_CYAN = 36
COLOR_GRAY = 90
COLOR_PINK = 95
COLOR_PREFIX = "\x1b[%dm"
def color(color_code: int) -> str:
return COLOR_PREFIX % color_code
def init():
from ..shared import glob
COLORS = {
logging.CRITICAL: "31",
logging.FATAL: "31",
logging.ERROR: "31",
logging.WARNING: "33",
logging.INFO: "32",
logging.DEBUG: "35",
logging.NOTSET: "37",
}
for k,v in COLORS.items():
logging.addLevelName(k, "\x1b[%sm%s\x1b[0m" % (v, " " * (8 - len(logging.getLevelName(k))) + logging.getLevelName(k)))
conf = glob.conf["logging"]
default_logging_fallback = False
if type(conf["level"]) is not int:
try:
conf["level"] = getattr(logging, conf["level"])
except:
conf["level"] = logging.INFO
default_logging_fallback = True
logging_handlers = [logging.StreamHandler(sys.stdout)]
if len(conf["filename"]):
logging_handlers.append(logging.FileHandler(conf["filename"]))
del conf["filename"]
logging.basicConfig(**conf, format=FORMAT_STR_NORMAL, handlers = logging_handlers)
if default_logging_fallback:
logging.warning("Invalid logging value in config! Defaulting to logging level INFO.")
logging.info("Logging level: %d" % conf["level"])
logging.debug("Logger configured")

1
sampy/raknet/__init__.py Normal file
View File

@@ -0,0 +1 @@
from . import bitstream

388
sampy/raknet/bitstream.py Normal file
View File

@@ -0,0 +1,388 @@
from __future__ import annotations
import ctypes as c
from array import array
from typing import Tuple, Union
from ..helpers import logging
logger = logging.getLogger(__name__)
DEBUG = (logger.root.level & logging.DEBUG) == 10
if DEBUG:
import inspect
BITS_TO_BYTES = lambda x: (((x)+7)>>3)
BYTES_TO_BITS = lambda x: ((x)<<3)
debug_wrapper_depth = 0
ignored_bits = []
class Bitstream:
def __init__(self, data: bytes = b""):
self._offset = 0
self._buffer = bytearray(data) # array("B", data)
def debug_wrapper(func):
def wrapper(self, *args, **kwargs):
if not DEBUG:
return func(self, *args, **kwargs)
global debug_wrapper_depth
#global ignored_bits
ignored_bits.clear()
curframe = inspect.currentframe()
calframe = inspect.getouterframes(curframe, 2)
#print(calframe)
copy = self.clone()
header_func = "%s%s" % (func.__name__, (*args, *kwargs))
header = " {BLUE}{header_func}{SPACE_FUNC}{CYAN}{caller}{SPACE_CALLER}{GREEN}{file}{YELLOW}@L{lineno}{RESET}".format(
header_func = header_func,
caller = calframe[1].function,
file = calframe[1].filename.split("\\")[-1],
lineno = calframe[1].lineno,
CYAN = logging.color(logging.COLOR_CYAN),
BLUE = logging.color(logging.COLOR_BLUE),
GREEN = logging.color(logging.COLOR_GREEN),
YELLOW = logging.color(logging.COLOR_YELLOW),
RESET = logging.color(logging.COLOR_RESET),
SPACE_FUNC = " " * (28 - len(header_func)),
SPACE_CALLER = " " * (32 - len(calframe[1].function)),
)
prefix = "%s%s%s" % (
logging.color(logging.COLOR_RED),
">" * debug_wrapper_depth,
logging.color(logging.COLOR_RESET)
)
logger.debug(prefix + header)
debug_wrapper_depth += 1
result = func(self, *args, **kwargs)
edits = self.debug_edits(copy)
debug_wrapper_depth -= 1
logger.debug(edits)
return result
return wrapper
@property
def offset(self) -> int:
return self._offset
@property
def length(self) -> int:
return len(self._buffer) << 3
def clone(self) -> Bitstream:
copy = Bitstream(self._buffer)
copy._offset = self._offset
return copy
def _can_access_bits(self, bits_to_access: int) -> bool:
return self._offset + bits_to_access <= len(self._buffer) << 3
def add_bits(self, bit_length: int):
if bit_length <= 0:
return
self._buffer.extend(b"\x00" * BITS_TO_BYTES(bit_length))
def align_read_to_byte_boundary(self):
if DEBUG:
ignored_from = self._offset
if self._offset:
self._offset += 8 - (((self._offset - 1) & 7) + 1)
if DEBUG:
ignored_bits.append((ignored_from, self._offset))
@debug_wrapper
def write_bit(self, value: bool) -> bool:
if not self._can_access_bits(1):
return False
mask = (1 << (7 - (self._offset % 8)))
if value:
self._buffer[self._offset >> 3] |= mask
else:
self._buffer[self._offset >> 3] &= ~mask
self._offset += 1
return True
@debug_wrapper
def read_bit(self) -> Tuple[bool, bool]:
if not self._can_access_bits(1):
return False, False
mask = (1 << (7 - (self._offset % 8)))
value = self._buffer[self._offset >> 3] & mask
self._offset += 1
return True, value > 0
@debug_wrapper
def write(self, value: bytes, bit_length: int) -> bool:
if not self._can_access_bits(bit_length):
return False
if len(value) << 3 < bit_length:
return False
byte_from = self._offset >> 3
byte_to = (self._offset + bit_length + 7) >> 3
bits_written = 0
for byte_index in range(byte_from, byte_to, 1):
byte = self._buffer[byte_index]
for bit_index in range(self._offset % 8, 8, 1):
mask = 1 << (7 - bit_index)
bit = value[bits_written >> 3] & (1 << ((7 - bits_written) % 8))
if bit:
byte |= mask
else:
byte &= ~mask
self._offset += 1
bits_written += 1
if bits_written >= bit_length:
break
self._buffer[byte_index] = byte
return True
@debug_wrapper
def read_int(self, bit_length: int) -> Tuple[bool, int]:
if not self._can_access_bits(bit_length):
return False, 0
byte_from = self._offset >> 3
byte_to = (self._offset + bit_length + 7) >> 3
# Since python doesnt have a max number size,
# we can read the bits we need and store them into a number,
# then convert the number to bytes (as they are all in the same for python)
value = 0
for byte_index in range(byte_from, byte_to, 1):
for bit_index in range(self._offset % 8, 8, 1):
mask = 1 << (7 - bit_index)
bit = self._buffer[self._offset >> 3] & (1 << ((7 - self._offset) % 8))
value = (value << 1) + bit
self._offset += 1
if self._offset >= bit_length:
break
#if self._offset % 8 == 0:
#value +=
return True, value
def read(self, bit_length: int) -> Tuple[bool, bytes]:
success, value = self.read_int(bit_length)
return success, value.to_bytes((bit_length + 7) >> 3, "big") # bytes(value)
@debug_wrapper
def write_compressed(self, data: bytes, bit_length: int, unsigned: bool = True) -> bool:
current_byte = (bit_length >> 3) - 1
byte_match = 0 if unsigned else 0xFF
while current_byte > 0:
if data[current_byte] == byte_match:
self.write_bit(True)
else:
self.write_bit(False)
self.write_bits(data, (current_byte + 1) << 3, True)
return True
if (unsigned and data[current_byte] & 0xF0 == 0x00) or (not unsigned and data[current_byte] & 0xF0 == 0xF0):
self.write_bit(True)
self.write_bits(data[current_byte:], 4, True)
else:
self.write_bit(False)
self.write_bits(data[current_byte:], 8, True)
return True
@debug_wrapper
def read_compressed(self, bit_length: int, unsigned: bool = True) -> Tuple[bool, bytes]:
current_byte = (bit_length >> 3) - 1
out = bytearray(current_byte + 1)
byte_match = 0 if unsigned else 0xFF
half_byte_match = 0 if unsigned else 0xF0
while current_byte > 0:
success, bit = self.read_bit()
if not success: # Cannot read the bit (end of stream)
return False, out
if bit:
out[current_byte] = byte_match
current_byte -= 1
else:
success, data = self.read_bits((current_byte + 1) << 3, True)
out[:len(data)] = data
if not success:
return False, out
return True, out
if self.offset + 1 > self.length:
return False, out
success, bit = self.read_bit()
if not success: # Cannot read the bit (end of stream)
return False, out
if bit:
success, data = self.read_bits(4, True)
out[current_byte:((current_byte + 4) + 7) >> 3] = data
if not success:
return False, out
out[current_byte] |= half_byte_match # Maybe recheck this in BitStream.cpp@L617; We have to set the high 4 bits since these are set to 0 by ReadBits
else:
success, data = self.read_bits(8, True)
out[current_byte:current_byte + 8] = data
if not success:
return False, out
return True, out
@debug_wrapper
def write_bits(self, data: bytes, bit_length: int, align_bits_to_right: bool = True) -> bool:
if bit_length <= 0:
return False
self.add_bits(bit_length) # TODO: Check if needed
bits_used_mod8 = self.offset & 7
offset = 0
while bit_length > 0:
byte = data[offset]
if bit_length < 8 and align_bits_to_right:
byte <<= 8 - bit_length
if bits_used_mod8 == 0:
self._buffer[self.offset >> 3] = byte
else:
self._buffer[self.offset >> 3] |= byte >> bits_used_mod8
if 8 - bits_used_mod8 < 8 and 8 - bits_used_mod8 < bit_length: # Eh.. why not bits_used_mod8 != 0?
self._buffer[(self.offset >> 3) + 1] = byte << (8 - bits_used_mod8)
if bit_length >= 8:
self._offset += 8
else:
self._offset += bit_length
bit_length -= 8
offset += 1
return True
@debug_wrapper
def read_bits(self, bit_length: int, align_bits_to_right: bool = True) -> Tuple[bool, bytes]:
if bit_length <= 0:
return False, b""
if not self._can_access_bits(bit_length):
return False, b""
out = bytearray(BITS_TO_BYTES(bit_length))
read_offset_mod8 = self._offset & 7
offset = 0
while bit_length > 0:
out[offset] |= (self._buffer[self._offset >> 3] << read_offset_mod8) & 255
if read_offset_mod8 > 0 and bit_length > 8 - read_offset_mod8:
out[offset] |= self._buffer[(self.offset >> 3) + 1] >> (8 - read_offset_mod8)
bit_length -= 8
if bit_length < 0:
if align_bits_to_right:
out[offset] >>= -bit_length
self._offset += 8 + bit_length
else:
self._offset += 8
offset += 1
return True, out
@debug_wrapper
def read_aligned_bytes(self, byte_length: int) -> Tuple[bool, bytes]:
if byte_length <= 0:
return False, b""
self.align_read_to_byte_boundary()
if self.offset + (byte_length << 3) > (len(self._buffer) << 3):
return False, b""
out = self._buffer[(self.offset >> 3):(self.offset >> 3) + byte_length]
self._offset += byte_length << 3
return True, out
def pretty(self) -> str:
b = bytearray(" " + " ".join(" ".join(format(c, "08b")) for c in self._buffer) + " ", "ascii")
m = self._offset * 2 + (self._offset >> 3) - (self._offset // (max(1, len(self._buffer)) * 8))
b[m] = 124
return b.decode()
def debug_edits(self, prev_state: Bitstream) -> str:
#logger.debug(self.pretty())
bits = [list(format(c, "08b")) for c in self._buffer]
prev_bits = [list(format(c, "08b")) for c in prev_state._buffer]
bits.append([])
start = prev_state._offset
end = self._offset
edits = {start: "P", end: "R"}
for s, e in ignored_bits:
t = [x for x in edits.keys() if x <= s]
if len(t):
edits[e] = edits[t[-1]]
else:
edits[e] = "R"
edits[s] = "S"
edits = sorted(edits.items())
edits.reverse()
for k, v in edits:
bits[k // 8][k % 8:k % 8] = v
#print(bits)
bits = " ".join("".join([y + " " if y in "01" else y for y in x]).strip() for x in bits)
bits = bits.replace("R", logging.color(logging.COLOR_RESET))
bits = bits.replace("P", logging.color(logging.COLOR_PINK))
bits = bits.replace("S", logging.color(logging.COLOR_RED))
return bits
def __repr__(self) -> str:
return "<Bitstream addr:0x%012x offset:%d len:%d>" % (id(self), self._offset, len(self._buffer) << 3)

4
sampy/server/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
from . import compression
from . import server
from .server import Server

View File

@@ -0,0 +1,61 @@
from ..shared import glob
from ..struct.server import ServerConfig
from typing import Tuple
import logging
logger = logging.getLogger(__name__)
# Found @ addr 0x004C88E0
LOOKUP_TABLE = b"\xb4b\x07\xe5\x9d\xafc\xdd\xe3\xd0\xcc\xfe\xdc\xdbk.j@\xabG\xc9\xd1S\xd5 \x91\xa5\x0eJ\xdf\x18\x89\xfdo%\x12\xb7\x13w\x00e6mI\xecW*\xa9\x11_\xfax\x95\xa4\xbd\x1e\xd9yD\xcd\xde\x81\xeb\t>\xf6\xee\xda\x7f\xa3\x1a\xa7-\xa6\xad\xc1F\x93\xd2\x1b\x9c\xaa\xd7NKML\xf3\xb84\xc0\xca\x88\xf4\x94\xcb\x0490\x82\xd6s\xb0\xbf\"\x01AnH,\xa8u\xb1\n\xae\x9f\'\x80\x10\xce\xf0)(\x85\r\x05\xf75\xbb\xbc\x15\x06\xf5`q\x03\x1f\xeaZ3\x92\x8d\xe7\x90[\xe9\xcf\x9e\xd3]\xed1\x1c\x0bR\x16Q\x0f\x86\xc5h\x9b!\x0c\x8bB\x87\xffO\xbe\xc8\xe8\xc7\xd4z\xe0U/\x8a\x8e\xba\x987\xe4\xb28\xa1\xb62\x83:{\x84<a\xfb\x8c\x14=C;\x1d\xc3\xa2\x96\xb3\xf8\xc4\xf2&+\xd8|\xfc#$f\xefidPTY\xf1\xa0t\xac\xc6}\xb5\xe6\xe2\xc2~g\x17^\xe1\xb9?lp\x08\x99EVv\xf9\x9a\x97\x19r\\\x02\x8fX"
class Compressor:
def __init__(self, config: ServerConfig):
self.config = config
def decompress(self, bytestream: bytes) -> bytes:
"""Decompress client packet.
This is actually a deobfuscation as there is no compression involved anymore
as zlib was removed and just swapped with this implementation after the leak.
Arguments:
bytestream {bytes} -- Bytes sent by client
"""
checksum, data = bytestream[0], bytearray(bytestream[1:])
data = self.xor_every_other_byte(self.get_port_xor_key(), data)
data = self.run_though_lookup_table(data)
if checksum != self.calc_checksum(data):
logger.error("Checksum failed!")
raise Exception("Checksum fail")
logger.debug("decompress -> %s" % data)
return bytes(data)
def xor_every_other_byte(self, xor: int, bytestream: bytearray) -> bytearray:
for i in range(1, len(bytestream), 2):
bytestream[i] ^= xor
return bytestream
def run_though_lookup_table(self, bytestream: bytearray) -> bytearray:
return bytes(LOOKUP_TABLE[b] for b in bytestream)
def calc_checksum(self, bytestream: bytearray) -> int:
checksum = 0
for byte in bytestream:
checksum ^= byte & 0xAA
return checksum
def get_port_xor_key(self) -> int:
return (self.config.port ^ 0xCCCC) & 0xFF
class StringCompressor:
def __init__(self):
pass
def decode_string(self, data: bytes) -> Tuple[str, int]:
pass

View File

@@ -2,23 +2,28 @@ import socket
import asyncio import asyncio
from select import select # This is straight up magic from select import select # This is straight up magic
from .struct.server import ServerConfig from ..struct.server import ServerConfig
from .client import Client from ..client import Client
from .compression import Compressor
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Server: class Server:
def __init__(self, config: ServerConfig): def __init__(self, config: ServerConfig):
self.config = config self.config = config
self.clients = {} self.clients = {}
self.rcon_clients = {} self.rcon_clients = {}
self.compressor = Compressor(self.config)
async def create_socket(self): async def create_socket(self):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind((self.config.host, self.config.port)) self.socket.bind((self.config.host, self.config.port))
self.socket.setblocking(0) # TODO: Check if needed? I dont fully understand this "feature" self.socket.setblocking(0)
logger.debug("Socket created") logger.debug("Socket created")
async def on_command(self, cmd: str): async def on_command(self, cmd: str):

View File

@@ -2,7 +2,8 @@ import os
import sys import sys
import json import json
import shutil import shutil
import logging
from ..helpers import logging
if not os.path.isfile("config.json"): if not os.path.isfile("config.json"):
shutil.copyfile(os.path.join(*__package__.split(".")[:-1], "default_config.json"), "config.json") shutil.copyfile(os.path.join(*__package__.split(".")[:-1], "default_config.json"), "config.json")
@@ -14,27 +15,4 @@ with open("config.json", "r") as f:
conf = config["sampy"] conf = config["sampy"]
conf_log = conf["logging"] conf_log = conf["logging"]
# Setup logger logging.init()
## fix for logging level
default_logging_fallback = False
if type(conf_log["level"]) is not int:
try:
conf_log["level"] = getattr(logging, conf_log["level"])
except:
conf_log["level"] = logging.INFO
default_logging_fallback = True
## logging settings
logging_handlers = [logging.StreamHandler(sys.stdout)]
if len(conf_log["filename"]):
logging_handlers.append(logging.FileHandler(conf_log["filename"]))
del conf_log["filename"]
logging.basicConfig(**conf_log, handlers = logging_handlers)
logger = logging.getLogger(__name__)
logger.debug("Configured logger")
if default_logging_fallback:
logger.warning("Invalid logging value in config! Defaulting to logging level INFO.")
logger.info("Logging level: %d" % conf_log["level"])

View File

@@ -1 +1,2 @@
from . import server from . import server
from . import packet

33
sampy/struct/packet.py Normal file
View File

@@ -0,0 +1,33 @@
class InternalPacket:
def __init__(self):
self.message_number = message_number # ushort
self.pack_number = pack_number # uint
self.priority = priority # int? this could be a byte (actually 2 bits is enough)
self.reliability = reliability # int? this could be a byte (actually 3 bits is enough)
self.ordering_channel = ordering_channel # uchar
self.ordering_index = ordering_index # ushort
self.split_packet_id = split_packet_id # ushort
self.split_packet_index = split_packet_index # uint
self.split_packet_count = split_packet_count # uint
self.creation_time = creation_time # long long
self.next_action_time = next_action_time # long long
self.data_bit_length = data_bit_length # uint
self.data = data # char*
self.histogram_marker = histogram_marker # uint
def get_bitstream_header_length(self) -> int:
bit_length = 32 # 2 * 2 * 8
bit_length += 3 # TODO: merge ^
if self.reliability in [1, 3, 4]:
bit_length += 5
bit_length += 16 # 2 * 8 TODO: merge ^
bit_length += 1 # TODO: merge ^
if self.split_packet_count > 0:
bit_length += 80 # (2 + 4 * 2) * 8
bit_length += 16 # TODO: merge ^
return bit_length

View File

@@ -16,7 +16,7 @@ class ServerConfig:
self.rcon_password = rcon_password self.rcon_password = rcon_password
self.max_players = max_players self.max_players = max_players
self.mode = mode self.mode = mode
self.language = language self.language = language

128
test.py Normal file
View File

@@ -0,0 +1,128 @@
from sampy.raknet.bitstream import Bitstream
from sampy.client.player import PlayerClient
import struct
class Packet:
def __init__(self, data: bytes, is_client: bool):
self.data = data
self.is_client = is_client
self.bitstream = Bitstream(bytearray.fromhex(self.data))
PACKETS = [
Packet("18 69 69", True),
Packet("1a 18 be", False),
Packet("18 71 d7", True),
Packet("19 00", False),
Packet("00 00 43 80 0b", True),
Packet("e3 00 00", False),
Packet("00 00 42 98 0c 11 33 30 45 39 33 39 33 33 36 39 42 35 36 38 43 32 00", False)
]
#client = PlayerClient(None, "127.0.0.1", 7777)
#client.state = 1
def pbs(bitstream: Bitstream, prefix: str = ""):
print(prefix, bitstream.pretty())
def deserialize(bitstream: Bitstream): # TODO: Something weird with range thing DS_RangeList.h@L92
success, count = bitstream.read_compressed(0x10, True)
if not success:
return False
count, = struct.unpack(b"<H", count)
min, max = 0, 0
for i in range(count):
_, max_equal_to_min = bitstream.read_bit()
success, min = bitstream.read_bits(0x10)
if not success:
return False
min, = struct.unpack(b"<H", min)
if not max_equal_to_min:
success, max = bitstream.read_bits(0x10)
if not success:
return False
max, = struct.unpack(b"<H", max)
if max < min:
return False
else:
max = min
return True
def on_player_packet(packet: bytes):
bitstream = Bitstream(packet)
pbs(bitstream, "Created")
_, has_acks = bitstream.read_bit()
pbs(bitstream, "has_acks")
if has_acks:
print("has_acks: True")
if not deserialize(bitstream):
return False
return
handled = handle_internal_packet(bitstream)
if not handled:
print("Internal packet were not handled")
def handle_internal_packet(bitstream: Bitstream) -> bool:
if bitstream.length - bitstream.offset < 0x10:
return False
_, message_number = bitstream.read_bits(0x10)
pbs(bitstream, "message_number")
_, reliability = bitstream.read_bits(0x04)
pbs(bitstream, "reliability")
if reliability in (7, 9, 10):
success, ordering_channel = bitstream.read_bits(0x05)
pbs(bitstream, "ordering_channel")
if not success:
return False
success, ordering_index = bitstream.read_bits(0x10)
pbs(bitstream, "ordering_index")
if not success:
return False
_, is_split_packet = bitstream.read_bit()
pbs(bitstream, "is_split_packet")
if is_split_packet:
print("Skipping split packet")
return
# TODO: set global split_packet index and count back to 0
# Something I dont understand yet
# TODO: ReadCompressed
#bitstream.offset += 1
success, data_bit_length = bitstream.read_compressed(0x10, True)
pbs(bitstream, "data_bit_length")
if not success:
return False
#_, unknown = bitstream.read(0x01)
#_, bit_length = bitstream.read(0x08)
##
#logger.debug("bit_length: %d" % bit_length)
#logger.debug("bitstream: %s" % bitstream)
data_bit_length, = struct.unpack(b"<H", data_bit_length)
print("data_bit_length:", data_bit_length)
# TODO: ReadAlignedBytes
#_, data = bitstream.read(bit_length)
_, data = bitstream.read_aligned_bytes((data_bit_length + 7) >> 3)
pbs(bitstream, "data")
print("data: %s" % data)
on_player_packet(PACKETS[-2].bitstream._buffer)