clientbot/client.py

406 lines
18 KiB
Python
Executable file
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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',
'': '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()