#!/usr/bin/python3 # -*- coding: utf-8 -*- # # script to emulate client khanat # # Copyright (C) 2019 AleaJactaEst # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # Ex.: ./client.py --khanat-host 172.17.0.3 -d --size-buffer-file 10241024 # Modifier les droits pour les nouveaux joueurs (accès à tout) # mysql -u root -e "use nel_ams_lib;UPDATE settings SET Value = 7 WHERE settings.Setting = 'Domain_Auto_Add';" import argparse import http.client import crypt import logging import os import os.path import sys import urllib.request import urllib.parse import tempfile #from enum import IntEnum #from ctypes import * import re import random import lzma #import socket #import xml.etree.ElementTree as ET #import hashlib #import time #import signal #from tools import BitStream #from tools import CCharacterSummary #from tools import CBitSet #from tools import getPowerOf2 #from tools import CFileChild #from tools import CFileList from tools import CFileContainer #from tools import Enum #from tools import World #from tools import CActionFactory #from tools import CSessionId #from tools import CGenericMultiPartTemp #from tools import CMainlandSummary #from tools import CAction #from tools import DecodeImpulse #from tools import CImpulseDecoder #from tools import CodeMsgXml from tools import CPersistentDataRecord from tools import ClientNetworkConnection from tools import CStringManager #INVALID_SLOT = 0xff LOGGER = 'Client' class ClientKhanat: def __init__(self, khanat_host, khanat_port_login = 40916, khanat_port_frontend = 47851, login="tester", password="tester", clientApp="Lirria", LanguageCode="fr", url="/login/r2_login.php", suffix = None, download_patch = False, show_patch_detail=False, size_buffer_file=1024): if suffix is None: suffix = str(random.randrange(1, 9999)) logging.getLogger(LOGGER).debug("suffix : %s" % suffix) self.download_patch = download_patch self.show_patch_detail = show_patch_detail self.khanat_host = khanat_host self.khanat_port_login = khanat_port_login self.khanat_port_frontend = khanat_port_frontend self.login = login + suffix self.password = password self.clientApp = clientApp self.LanguageCode = LanguageCode self.url = url self.cookie, self.fsaddr, self.ringmainurl, self.fartp, self.stat, self.r2serverversion, self.r2backuppatchurl, self.r2patchurl = None, None, None, None, None, None, None, None self.tempdir = tempfile.TemporaryDirectory(".khanat") logging.getLogger(LOGGER).debug("Temporary directory:%s" % self.tempdir) self.khanat_idx = CPersistentDataRecord.CPersistentDataRecord() self.UserAddr, self.UserKey, self.UserId = None, None, None self.clientNetworkConnection = ClientNetworkConnection.ClientNetworkConnection(self.khanat_host, self.khanat_port_frontend, self.login) self.size_buffer_file = size_buffer_file self.cFileContainer = CFileContainer.CFileContainer() def createAccount(self): conn = http.client.HTTPConnection(host=self.khanat_host, port=self.khanat_port_login) cmd = "/ams/index.php?page=register" headers = {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language' : 'en-US', 'Connection': 'keep-alive', 'DNT': '1', 'Cookie': 'PHPSESSID=lsoumn9f0ljgm3vo3hgjdead03', 'Host': self.khanat_host+':'+ str(self.khanat_port_login), 'Referer': 'http://' + self.khanat_host+':'+ str(self.khanat_port_login) + '/ams/index.php?page=register', 'Upgrade-Insecure-Requests': '1', 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; rv:6.0) Gecko/20100101 Firefox/6.0', '': 'application/x-www-form-urlencoded'} headers = {'Content-Type': 'application/x-www-form-urlencoded'} params = urllib.parse.urlencode({'Username': self.login, 'Password': self.password, 'ConfirmPass': self.password, 'Email': self.login+'@khaganat.net', 'TaC': 'on', 'function': 'add_user'}) logging.getLogger(LOGGER).debug("POST %s" % (cmd)) print( "host%s, port:%s" % (self.khanat_host, str(self.khanat_port_login))) print("cmd:%s" % cmd) print("params:%s" % params) print("headers:%s" % headers) conn.request("POST", cmd, params, headers) response = conn.getresponse() if ( int(response.status) == 302 ): conn.close() logging.getLogger(LOGGER).info("Account created : %s" % self.login) return elif ( int(response.status) != 200 ): logging.getLogger(LOGGER).error("Impossible to create account (return code:" + str(response.status) + ")") sys.exit(2) ret = response.read() print("ret:%s" % ret) conn.close() ret2 = ret.decode() try: state, comment = ret2.split(":", 2) except: state = 1 comment = "" if int(state) != 1: logging.getLogger(LOGGER).error("Impossible to create account (state:" + state + ", comment:" + comment.strip() + ")") sys.exit(2) errordetected = False for line in ret2.split('\n'): m = re.search("((?P.*) Error )(?P[^.]+)", line) if m: if m.group('comment') == 'Username ' + self.login + ' is in use': continue if m.group('comment') == 'Email is in use': continue logging.getLogger(LOGGER).error('Impossible to create account: field:%s (%s)' % (m.group('type'), m.group('comment'))) errordetected = True if errordetected: sys.exit(2) logging.getLogger(LOGGER).info("Reuse account : %s" % self.login) sys.exit(0) def connectR2(self): conn = http.client.HTTPConnection(host=self.khanat_host, port=self.khanat_port_login) cmd = self.url + "?cmd=ask&cp=2&login=" + self.login + "&lg=" + self.LanguageCode logging.getLogger(LOGGER).debug("GET %s" % (cmd)) conn.request("GET", cmd) response = conn.getresponse() if ( int(response.status) != 200 ): logging.getLogger(LOGGER).error("Impossible to get salt (return code:" + str(response.status) + ")") sys.exit(2) ret = response.read() conn.close() try: state, salt = ret.decode().split(":", 1) except UnicodeDecodeError: try: state, salt = ret.decode(encoding='cp1252').split(":", 1) except UnicodeDecodeError: logging.getLogger(LOGGER).error("Impossible to read output login") sys.exit(2) if int(state) != 1: logging.getLogger(LOGGER).error("Impossible to get salt (state:" + state + ")") cryptedPassword = crypt.crypt(self.password, salt) conn = http.client.HTTPConnection(host=self.khanat_host, port=self.khanat_port_login) cmd = self.url + "?cmd=login&login=" + self.login + "&password=" + cryptedPassword + "&clientApplication=" + self.clientApp + "&cp=2" + "&lg=" + self.LanguageCode logging.getLogger(LOGGER).debug("GET %s" % (cmd)) conn.request("GET", cmd) response = conn.getresponse() logging.getLogger(LOGGER).debug("%s %s" %(response.status, response.reason)) ret = response.read() logging.getLogger(LOGGER).debug(ret) try: line = ret.decode().split('\n') except UnicodeDecodeError: try: line = ret.decode(encoding='cp1252').split('\n') except UnicodeDecodeError: logging.getLogger(LOGGER).error("Impossible to read output login") sys.exit(2) logging.getLogger(LOGGER).debug(line[0]) logging.getLogger(LOGGER).debug("line 0 '%s'" % line[0]) logging.getLogger(LOGGER).debug("line 1 '%s'" % line[1]) try: state, self.cookie, self.fsaddr, self.ringmainurl, self.fartp, self.stat = line[0].split("#", 6) except: try: state, self.cookie, self.fsaddr, self.ringmainurl, self.fartp = line[0].split("#", 5) self.stat = 0 except: state, error = line[0].split(":", 1) if int(state) != 1: logging.getLogger(LOGGER).error(error) sys.exit(2) self.r2serverversion, self.r2backuppatchurl, self.r2patchurl = line[1].split("#") logging.getLogger(LOGGER).debug("%s %s %s %s %s %s %s %s %s" % (state, self.cookie, self.fsaddr, self.ringmainurl, self.fartp, self.stat, self.r2serverversion, self.r2backuppatchurl, self.r2patchurl)) self.UserAddr, self.UserKey, self.UserId = [ int(x, 16) for x in self.cookie.split('|') ] conn.close() logging.getLogger(LOGGER).info("Login Ok") self.clientNetworkConnection.cookiesInit(self.UserAddr, self.UserKey, self.UserId) def downloadFileUrl(self, source, dest): logging.getLogger(LOGGER).info("Download %s (destination:%s)" % (source, dest)) with urllib.request.urlopen(source) as conn : header = conn.getheaders() file_size = 0 for key, value in header: if key == 'Content-Length': file_size = int(value) break logging.getLogger(LOGGER).debug("size:%d", file_size) file_size_dl = 0 block_size = self.size_buffer_file # 1024 with open(dest, 'wb') as fp: while True: buffer = conn.read(block_size) if not buffer: break file_size_dl += len(buffer) fp.write(buffer) logging.getLogger(LOGGER).debug("Download %s %10d [%6.2f%%]" % (source, file_size_dl, file_size_dl * 100. / file_size)) fp.close() logging.getLogger(LOGGER).debug("Downloaded %s (%d)" % (source, file_size)) def getServerFile(self, name, bZipped = False, specifyDestName = None): srcName = name if specifyDestName: dstName = specifyDestName else: dstName = os.path.basename(name) if bZipped: srcName += ".ngz" dstName += ".ngz" logging.getLogger(LOGGER).info("Download %s (destination:%s, zip:%d)" % (srcName, dstName, bZipped)) dstName = os.path.join(self.tempdir.name, dstName) self.downloadFileUrl( 'http://' + self.r2patchurl + '/' + srcName, dstName) return dstName def downloadAllPatch(self): # TODO - check where client search file to download for file in self.khanat_idx.CBNPFile: tmp = self.getServerFile("%05d/%s.lzma" % (int(self.r2serverversion), file.FileName), False, "") with lzma.open(tmp) as fin: dstName = os.path.join(self.tempdir.name, file.FileName) with open(dstName, "wb") as fout: data = fin.read() fout.write(data) logging.getLogger(LOGGER).info("%s" % dstName) os.remove(tmp) # khanat-opennel-code/code/ryzom/client/src/login_patch.cpp # void CCheckThread::run () FilesToPatch = [] for file in self.khanat_idx.CBNPFile: FilesToPatch.append(file) # Here we got all the files to patch in FilesToPatch and all the versions that must be obtained Now we have to get the optional categories OptionalCat = [] for category in self.khanat_idx.Categories: if category._IsOptional: for file in category._Files: bAdded = False for file2 in FilesToPatch: if file2 == file: OptionalCat.append(category._Name) bAdded = True break if bAdded: break # For all categories that required an optional category if the cat required is present the category that reference it must be present for category in self.khanat_idx.Categories: if category._IsOptional and not len(category._CatRequired) == 0: bFound = False for cat in OptionalCat: if category._Name == cat: bFound = True break if bFound: for cat in OptionalCat: if category._CatRequired == cat: OptionalCat.append(category._Name) break # Delete categories optional cat that are hidden for category in self.khanat_idx.Categories: if category._IsOptional and category._Hidden: for cat in OptionalCat: if category._Name == cat: OptionalCat.remove(category._Name) break # Get all extract to category and check files inside the bnp with real files for category in self.khanat_idx.Categories: if len(category._UnpackTo) != 0: for file in category._Files: # TODO # readHeader() pass def DownloadMinimum(self): logging.getLogger(LOGGER).debug("-" * 80) for file in self.khanat_idx.CBNPFile: if file.FileName != "kh_server.bnp": continue tmp = self.getServerFile("%05d/%s.lzma" % (int(self.r2serverversion), file.FileName), False, "") with lzma.open(tmp) as fin: dstName = os.path.join(self.tempdir.name, file.FileName) with open(dstName, "wb") as fout: data = fin.read() fout.write(data) logging.getLogger(LOGGER).info("%s" % dstName) os.remove(tmp) def Emulate(self): self.createAccount() self.connectR2() # download patch self.ryzomidx = self.getServerFile("%05d/ryzom_%05d.idx" % (int(self.r2serverversion), int(self.r2serverversion)), False, "") self.khanat_idx.readFromBinFile(self.ryzomidx) self.khanat_idx.CProductDescriptionForClient_apply() # Show detail patch if self.show_patch_detail: self.khanat_idx.decrypt_token() self.khanat_idx.show() # Todo analyze patch and download if necessary or update if incremental - see category # Download all file in patch - login_patch.cpp:2578 # void CPatchThread::processFile (CPatchManager::SFileToPatch &rFTP) if self.download_patch: self.downloadAllPatch() else: self.DownloadMinimum() self.cFileContainer = CFileContainer.CFileContainer() self.cFileContainer.addSearchPath(self.tempdir.name) msgRawXml = self.cFileContainer.getdata("msg.xml").decode() databaseRawXml = self.cFileContainer.getdata("database.xml").decode() self.clientNetworkConnection.EmulateFirst(msgRawXml, databaseRawXml) def main(): FORMAT = '%(asctime)-15s %(levelname)s %(filename)s:%(lineno)d %(message)s' logging.basicConfig(format=FORMAT) logger = [] logger.append(logging.getLogger(LOGGER)) #logger.append(logging.getLogger(CImpulseDecoder.LOGGER)) #logger.append(logging.getLogger(DecodeImpuls.LOGGER)) #logger.append(logging.getLogger(BitStream.LOGGER)) logger.append(logging.getLogger(CStringManager.LOGGER)) logger.append(logging.getLogger(CPersistentDataRecord.LOGGER)) logger.append(logging.getLogger(ClientNetworkConnection.LOGGER)) logger.append(logging.getLogger(LOGGER)) parser = argparse.ArgumentParser() parser.add_argument("--khanat-host", help="khanat host to auhtenticate", default='localhost') parser.add_argument("--suffix", help="define suffix") parser.add_argument("-d", "--debug", help="show debug message", action='store_true') parser.add_argument("-p", "--download-patch", help="show debug message", action='store_true') parser.add_argument("-s", "--show-patch-detail", help="show debug message", action='store_true') parser.add_argument("--size-buffer-file", help="size buffer to download file", type=int, default=1024) parser.add_argument("--khanat-port-login", help="port http login", type=int, default=40916) parser.add_argument("--khanat-port-frontend", help="port UDP frontend", type=int, default=47851) args = parser.parse_args() if args.debug: level = logging.getLevelName('DEBUG') else: level = logging.getLevelName('INFO') for logid in logger: logid.setLevel(level) client = ClientKhanat(args.khanat_host, khanat_port_login=args.khanat_port_login, khanat_port_frontend=args.khanat_port_frontend, suffix=args.suffix, download_patch=args.download_patch, show_patch_detail=args.show_patch_detail, size_buffer_file=args.size_buffer_file) client.Emulate() logging.getLogger(LOGGER).info("End") if __name__ == "__main__": #TestBitStream() #TestCBitSet() main()