mirror of
https://port.numenaute.org/aleajactaest/clientbot.git
synced 2024-12-24 02:18:45 +00:00
406 lines
18 KiB
Python
Executable file
406 lines
18 KiB
Python
Executable file
#!/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 <http://www.gnu.org/licenses/>.
|
||
|
||
# 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',
|
||
'Content-Type': '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("(<strong>(?P<type>.*) Error</strong> )(?P<comment>[^.]+)", 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()
|