[Refactor] Link Play TCP data transmission

- Code refactor of Link Play TCP data transmission for better security and scalability
This commit is contained in:
Lost-MSth
2023-12-03 00:38:43 +08:00
parent 3e93082a3c
commit 150686d9f8
7 changed files with 222 additions and 75 deletions

View File

@@ -23,6 +23,7 @@ class Config:
LINKPLAY_TCP_PORT = 10901
LINKPLAY_AUTHENTICATION = 'my_link_play_server'
LINKPLAY_DISPLAY_HOST = ''
LINKPLAY_TCP_SECRET_KEY = '1145141919810'
SSL_CERT = ''
SSL_KEY = ''

View File

@@ -56,6 +56,8 @@ class Constant:
LINKPLAY_TCP_PORT = Config.LINKPLAY_TCP_PORT
LINKPLAY_UDP_PORT = Config.LINKPLAY_UDP_PORT
LINKPLAY_AUTHENTICATION = Config.LINKPLAY_AUTHENTICATION
LINKPLAY_TCP_SECRET_KEY = Config.LINKPLAY_TCP_SECRET_KEY
LINKPLAY_TCP_MAX_LENGTH = 0x0FFFFFFF
# Well, I can't say a word when I see this.
FINALE_SWITCH = [

View File

@@ -1,10 +1,12 @@
import socket
from base64 import b64decode, b64encode
from json import dumps, loads
from core.error import ArcError, Timeout
from .constant import Constant
from .user import UserInfo
from .util import aes_gcm_128_decrypt, aes_gcm_128_encrypt
socket.setdefaulttimeout(Constant.LINKPLAY_TIMEOUT)
@@ -86,53 +88,106 @@ class Room:
class RemoteMultiPlayer:
TCP_AES_KEY = Constant.LINKPLAY_TCP_SECRET_KEY.encode(
'utf-8').ljust(16, b'\x00')[:16]
def __init__(self) -> None:
self.user: 'Player' = None
self.room: 'Room' = None
self.data_recv: tuple = None
self.data_recv: 'dict | list' = None
def to_dict(self) -> dict:
return dict(self.room.to_dict(), **self.user.to_dict())
@staticmethod
def tcp(data: str) -> str:
def tcp(data: bytes) -> bytes:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((Constant.LINKPLAY_HOST,
Constant.LINKPLAY_TCP_PORT))
sock.sendall(bytes(data + "\n", "utf-8"))
sock.sendall(data)
try:
received = str(sock.recv(1024), "utf-8").strip()
cipher_len = int.from_bytes(sock.recv(8), byteorder='little')
if cipher_len > Constant.LINKPLAY_TCP_MAX_LENGTH:
raise ArcError(
'Too long body from link play server', status=400)
iv = sock.recv(12)
tag = sock.recv(12)
ciphertext = sock.recv(cipher_len)
received = aes_gcm_128_decrypt(
RemoteMultiPlayer.TCP_AES_KEY, b'', iv, ciphertext, tag)
except socket.timeout as e:
raise Timeout(
'Timeout when waiting for data from link play server.', status=400) from e
# print(received)
return received
def data_swap(self, data: tuple) -> tuple:
def data_swap(self, data: dict) -> dict:
received = self.tcp(Constant.LINKPLAY_AUTHENTICATION +
'|' + '|'.join([str(x) for x in data]))
iv, ciphertext, tag = aes_gcm_128_encrypt(
self.TCP_AES_KEY, dumps(data).encode('utf-8'), b'')
send_data = Constant.LINKPLAY_AUTHENTICATION.encode(
'utf-8') + len(ciphertext).to_bytes(8, byteorder='little') + iv + tag[:12] + ciphertext
recv_data = self.tcp(send_data)
self.data_recv = loads(recv_data)
self.data_recv = received.split('|')
if self.data_recv[0] != '0':
code = int(self.data_recv[0])
code = self.data_recv['code']
if code != 0:
raise ArcError(f'Link Play error code: {code}', code, status=400)
return self.data_recv
# if self.data_recv[0] != '0':
# code = int(self.data_recv[0])
# raise ArcError(f'Link Play error code: {code}', code, status=400)
# @staticmethod
# def tcp(data: str) -> str:
# with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
# sock.connect((Constant.LINKPLAY_HOST,
# Constant.LINKPLAY_TCP_PORT))
# send_data =
# sock.sendall(bytes(data + "\n", "utf-8"))
# try:
# received = str(sock.recv(1024), "utf-8").strip()
# except socket.timeout as e:
# raise Timeout(
# 'Timeout when waiting for data from link play server.', status=400) from e
# # print(received)
# return received
# def data_swap(self, data: tuple) -> tuple:
# received = self.tcp(Constant.LINKPLAY_AUTHENTICATION +
# '|' + '|'.join([str(x) for x in data]))
# self.data_recv = received.split('|')
# if self.data_recv[0] != '0':
# code = int(self.data_recv[0])
# raise ArcError(f'Link Play error code: {code}', code, status=400)
def create_room(self, user: 'Player' = None) -> None:
'''创建房间'''
if user is not None:
self.user = user
user.select_user_one_column('name')
self.data_swap((1, self.user.name, b64encode(
self.user.song_unlock).decode('utf-8')))
self.data_swap({
'endpoint': 'create_room',
'data': {
'name': self.user.name,
'song_unlock': b64encode(self.user.song_unlock).decode('utf-8')
}
})
self.room = Room()
self.room.room_code = self.data_recv[1]
self.room.room_id = int(self.data_recv[2])
x = self.data_recv['data']
self.room.room_code = x['room_code']
self.room.room_id = int(x['room_id'])
self.room.song_unlock = self.user.song_unlock
self.user.token = int(self.data_recv[3])
self.user.key = b64decode(self.data_recv[4])
self.user.player_id = int(self.data_recv[5])
self.user.token = int(x['token'])
self.user.key = b64decode(x['key'])
self.user.player_id = int(x['player_id'])
def join_room(self, room: 'Room' = None, user: 'Player' = None) -> None:
'''加入房间'''
@@ -142,23 +197,37 @@ class RemoteMultiPlayer:
self.room = room
self.user.select_user_one_column('name')
self.data_swap(
(2, self.user.name, b64encode(self.user.song_unlock).decode('utf-8'), room.room_code))
self.room.room_code = self.data_recv[1]
self.room.room_id = int(self.data_recv[2])
self.room.song_unlock = b64decode(self.data_recv[6])
self.user.token = int(self.data_recv[3])
self.user.key = b64decode(self.data_recv[4])
self.user.player_id = int(self.data_recv[5])
self.data_swap({
'endpoint': 'join_room',
'data': {
'name': self.user.name,
'song_unlock': b64encode(self.user.song_unlock).decode('utf-8'),
'room_code': self.room.room_code
}
})
x = self.data_recv['data']
self.room.room_code = x['room_code']
self.room.room_id = int(x['room_id'])
self.room.song_unlock = b64decode(x['song_unlock'])
self.user.token = int(x['token'])
self.user.key = b64decode(x['key'])
self.user.player_id = int(x['player_id'])
def update_room(self, user: 'Player' = None) -> None:
'''更新房间'''
if user is not None:
self.user = user
self.data_swap((3, self.user.token))
self.data_swap({
'endpoint': 'update_room',
'data': {
'token': self.user.token
}
})
self.room = Room()
self.room.room_code = self.data_recv[1]
self.room.room_id = int(self.data_recv[2])
self.room.song_unlock = b64decode(self.data_recv[5])
self.user.key = b64decode(self.data_recv[3])
self.user.player_id = int(self.data_recv[4])
x = self.data_recv['data']
self.room.room_code = x['room_code']
self.room.room_id = int(x['room_id'])
self.room.song_unlock = b64decode(x['song_unlock'])
self.user.key = b64decode(x['key'])
self.user.player_id = int(x['player_id'])

View File

@@ -1,9 +1,30 @@
import hashlib
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from datetime import date
from time import mktime
def aes_gcm_128_encrypt(key, plaintext, associated_data):
iv = os.urandom(12)
encryptor = Cipher(
algorithms.AES(key),
modes.GCM(iv, min_tag_length=12),
).encryptor()
encryptor.authenticate_additional_data(associated_data)
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
return (iv, ciphertext, encryptor.tag)
def aes_gcm_128_decrypt(key, associated_data, iv, ciphertext, tag):
decryptor = Cipher(
algorithms.AES(key),
modes.GCM(iv, tag, min_tag_length=12),
).decryptor()
decryptor.authenticate_additional_data(associated_data)
return decryptor.update(ciphertext) + decryptor.finalize()
def md5(code: str) -> str:
# md5加密算法
code = code.encode()