Files

234 lines
7.6 KiB
Python

import asyncio
import websockets
import base64
import time
import json
import requests
from aiortc.sdp import candidate_from_sdp, candidate_to_sdp
from aiortc.contrib.signaling import object_from_string, object_to_string
from aiortc import RTCIceCandidate, RTCPeerConnection, RTCSessionDescription
from aiortc.contrib.signaling import BYE, add_signaling_arguments, create_signaling
from aiortc.contrib.media import MediaBlackhole, MediaPlayer, MediaRecorder, MediaRelay
relay = MediaRelay()
sub_key = None
def login(phonenumber, code=None, token_file="token"):
"""Request an SMS code (when code is None) or exchange it for a token."""
if "-" not in phonenumber:
raise ValueError("phone number must use the '<country-code>-<number>' form")
if code is None:
response = requests.post(
"https://n.cg.163.com/api/v1/phone-captchas/" + phonenumber,
timeout=15,
)
response.raise_for_status()
print("input the code received by your phone")
code = input().strip()
country_code, phone = phonenumber.split("-", 1)
headers = {"Content-Type": "application/json;charset=utf-8"}
data = {
"auth_method": "phone-captcha",
"ctcode": country_code,
"phone": phone,
"captcha": str(code).strip(),
"device_info": {
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/140 Safari/537.36",
"appVersion": "5.0 (X11)",
"codecs": ["h264", "vp8", "vp9"],
},
}
response = requests.post(
"https://n.cg.163.com/api/v1/tokens",
headers=headers,
json=data,
timeout=15,
)
response.raise_for_status()
user_obj = response.json()
token = user_obj.get("token")
if not token:
raise RuntimeError("login response did not contain a token")
with open(token_file, "w", encoding="utf-8") as token_handle:
token_handle.write(token)
try:
import os
os.chmod(token_file, 0o600)
except OSError:
pass
print("login ok!")
return token
def encode_mess(message):
global sub_key
mess = str.encode(message)
m = b""
for i in mess:
m += ((i+sub_key)%256).to_bytes(1, byteorder='big')
res = base64.b64encode(m)
return res
def decode_mess(message):
try:
mess = base64.b64decode(message)
except:
return message
global sub_key
if sub_key == None:
fbyte = mess[0]
sbyte = mess[1]
for j in range(0, 256):
if(chr((fbyte-j)%256)=="[" and chr((sbyte-j)%256) == "{" and chr((mess[2]-j)%256) == "\""):
sub_key = j
break
if chr((fbyte-j)%256) == "{" and chr((sbyte-j)%256) == "\"":
sub_key = j
break
m = ""
for i in mess:
m += chr((i-sub_key)%256)
return m
def get_basic_info(token):
headers = {"Authorization": "Bearer " + token}
response = requests.get(
"https://n.cg.163.com/api/v2/users/@me",
headers=headers,
timeout=15,
)
response.raise_for_status()
info = json.loads(decode_mess(response.text))
return info["yunxin_account"]["accid"]
def request_ticket(token, game_code, regions=None, codecs=None, width=1280, height=720):
if regions is None:
regions = ["hdcz", "hbsjz"]
if codecs is None:
codecs = ["h264", "vp8", "vp9"]
req_obj = {
"regions": regions,
"game_code": game_code,
"codecs": codecs,
"width": width,
"height": height
}
global sub_key
req_str = encode_mess(json.dumps(req_obj).replace("'","\""))
headers = {
"Authorization": "Bearer " +token,
"Content-Type": "application/octet-stream"
}
response = requests.post(
"https://n.cg.163.com/api/v2/tickets",
headers=headers,
data=req_str,
timeout=15,
)
if not response.ok:
try:
detail = decode_mess(response.text)
except Exception:
detail = ""
raise RuntimeError(f"ticket request failed with HTTP {response.status_code}: {detail[:500]}")
info_obj = json.loads(decode_mess(response.text))
return info_obj["gateway_url"]
def find_region(token, game_code):
headers = {"Authorization": "Bearer " + token}
response = requests.get(
"https://n.cg.163.com/api/v2/media-servers",
params={"game_code": game_code},
headers=headers,
timeout=15,
)
response.raise_for_status()
regions = json.loads(decode_mess(response.text))
return [item["region"] for item in regions]
def exit_game(token, game_code):
headers = {"Authorization": "Bearer " + token}
response = requests.delete(
f"https://n.cg.163.com/api/v2/users/@me/games-playing/{game_code}",
headers=headers,
timeout=15,
)
response.raise_for_status()
return response.text
async def connect(token, game_code, w=1280, h=720, quality="high", codecs=None, platform=0, fps="30"):
if codecs is None:
codecs = ["h264", "vp8", "vp9"]
user_id = await asyncio.to_thread(get_basic_info, token)
regions = await asyncio.to_thread(find_region, token, game_code)
uri = await asyncio.to_thread(
request_ticket,
token,
game_code,
regions,
codecs,
w,
h,
)
websocket = await websockets.connect(uri)
auth_obj = {
"id": str(int(round(time.time() * 1000))),
"op": "auth",
"data": {
"user_id": user_id,
"token": token,
"game_code": game_code,
"w": w,
"h": h,
"quality": quality,
"codecs": codecs,
"platform": platform,
"fps": fps
}
}
auth_str = json.dumps(auth_obj)
await websocket.send(auth_str)
auth_res = await websocket.recv()
auth_res = decode_mess(auth_res)
try:
de_res = json.loads(auth_res)
except (TypeError, json.JSONDecodeError) as exc:
await websocket.close()
raise RuntimeError("failed to decode cloud-game auth response") from exc
if de_res.get("op") != "offer":
error_message = de_res.get("data", {}).get("errmsg", "unexpected signaling response")
await websocket.close()
raise RuntimeError(error_message)
new_sdp = {}
new_sdp["type"] = de_res["op"]
new_sdp["sdp"] = de_res["data"]["sdp"]
new_sdp = json.dumps(new_sdp)
return new_sdp, websocket
def channel_log(channel, t, message):
print("channel(%s) %s %s" % (channel.label, t, message))
async def send_action(sock, action):
action = encode_mess(json.dumps(action).replace("'","\""))
await sock.send(action)
pong_waiter = await sock.ping()
await pong_waiter
def pack_message(cmd, data):
if cmd == "mm": # move mouse, data: x, y
action = {"id":str(int(round(time.time() * 1000))),"op":"input","data":{"cmd":"1 %d %d 0" % (data["x"], data["y"])}}
elif cmd == "cm": # click mouse, data: x, y
action = {"id":str(int(round(time.time() * 1000))),"op":"input","data":{"cmd":"3 %d %d 0" % (data["x"], data["y"])}}
elif cmd == "ip": # keyboard input, data: single keyborad word
action = {"id":str(int(round(time.time() * 1000))),"op":"input","data":{"cmd":"5 %s" % data["word"]}}
else:
action = {}
return action
#print(decode_mess("2YDHwoCYgI+VlI6SlpeTk4+Uj5eAioDNzoCYgMfMztPSgIqAwr/Sv4CY2YDBy8KAmICRfpaOln6TkZB+joDb2w=="))