Merge pull request #197 from sepalani/master

General Improvement
This commit is contained in:
polaris-
2015-09-06 11:50:11 -04:00
25 changed files with 3525 additions and 1775 deletions

View File

@@ -11,3 +11,12 @@ Whether it's to add new features or clean up existing code, we could always use
Open source projects referenced during the creation of this project: [OpenSpy Core](https://github.com/sfcspanky/Openspy-Core/) | [Luigi Auriemma's Gslist and enctypex_decoder](http://aluigi.altervista.org/papers.htm)
Instructions for setting up your own server can be found [here](https://github.com/polaris-/dwc_network_server_emulator/wiki/Setting-up-a-server-from-a-fresh-installation-of-Linux).
##Requirements:
Python 2.7
Twisted
zope.interface (Twisted dependency)
pywin32 (Twisted dependency)
Apache (or your favorite httpd)
PHP5

View File

@@ -1,45 +1,44 @@
# DWC Network Server Emulator
# Copyright (C) 2014 SMTDDR
# Copyright (C) 2014 kyle95wm
# Copyright (C) 2014 AdmiralCurtiss
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""DWC Network Server Emulator
Copyright (C) 2014 SMTDDR
Copyright (C) 2014 kyle95wm
Copyright (C) 2014 AdmiralCurtiss
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from twisted.web import server, resource
from twisted.internet import reactor
from twisted.internet.error import ReactorAlreadyRunning
import base64
import codecs
import codecs
import sqlite3
import collections
import json
import time
import datetime
import os.path
import logging
import other.utils as utils
import gamespy
import gamespy.gs_utility as gs_utils
import dwc_config
logger_output_to_console = True
logger_output_to_file = True
logger_name = "AdminPage"
logger_filename = "admin_page.log"
logger = utils.create_logger(logger_name, logger_filename, -1, logger_output_to_console, logger_output_to_file)
logger = dwc_config.get_logger('AdminPage')
_, port = dwc_config.get_ip_port('AdminPage')
#Example of adminpageconf.json
# Example of adminpageconf.json
#
# {"username":"admin","password":"opensesame"}
#
@@ -53,46 +52,51 @@ admin_password = None
if os.path.exists('adminpageconf.json'):
try:
adminpageconf = json.loads(file('adminpageconf.json').read().strip())
admin_username = str(adminpageconf['username'])
admin_password = str(adminpageconf['password'])
admin_username = str(adminpageconf['username'])
admin_password = str(adminpageconf['password'])
except Exception as e:
logger.log(logging.WARNING, "Couldn't read adminpageconf.json. Admin page will not be available.")
logger.log(logging.WARNING,
"Couldn't read adminpageconf.json. "
"Admin page will not be available.")
logger.log(logging.WARNING, str(e))
adminpageconf = None
admin_username = None
admin_password = None
else:
logger.log(logging.INFO, "adminpageconf.json not found. Admin page will not be available.")
logger.log(logging.INFO,
"adminpageconf.json not found. "
"Admin page will not be available.")
class AdminPage(resource.Resource):
isLeaf = True
def __init__(self,adminpage):
def __init__(self, adminpage):
self.adminpage = adminpage
def get_header(self, title = None):
def get_header(self, title=None):
if not title:
title = 'AltWfc Admin Page'
s = (
'<html>'
'<head>'
'<title>' + title + '</title>'
'</head>'
'<body>'
'<p>'
'<a href="/banhammer">All Users</a> | '
'<a href="/consoles">Consoles</a> | '
'<a href="/banlist">Active Bans</a> '
'</p>'
)
s = """
<html>
<head>
<title>%s</title>
</head>
<body>
<p>
%s | %s | %s
</p>
""" % (title,
'<a href="/banhammer">All Users</a>',
'<a href="/consoles">Consoles</a>',
'<a href="/banlist">Active Bans</a>')
return s
def get_footer(self):
s = (
'</body>'
'</html>'
)
s = """
</body>
</html>
"""
return s
def is_authorized(self, request):
@@ -101,15 +105,19 @@ class AdminPage(resource.Resource):
error_message = "Authorization required!"
address = request.getClientIP()
try:
expected_auth = base64.encodestring(admin_username+":"+admin_password).strip()
actual_auth = request.getAllHeaders()['authorization'].replace("Basic ","").strip()
expected_auth = base64.encodestring(
admin_username + ":" + admin_password
).strip()
actual_auth = request.getAllHeaders()['authorization'] \
.replace("Basic ", "") \
.strip()
if actual_auth == expected_auth:
logger.log(logging.INFO,address+" Auth Success")
logger.log(logging.INFO, "%s Auth Success", address)
is_auth = True
except Exception,e:
logger.log(logging.INFO,address+" Auth Error: "+str(e))
except Exception as e:
logger.log(logging.INFO, "%s Auth Error: %s", address, str(e))
if not is_auth:
logger.log(logging.INFO,address+" Auth Failure")
logger.log(logging.INFO, "%s Auth Failure", address)
request.setResponseCode(response_code)
request.setHeader('WWW-Authenticate', 'Basic realm="ALTWFC"')
request.write(error_message)
@@ -121,33 +129,43 @@ class AdminPage(resource.Resource):
gameid = request.args['gameid'][0].upper().strip()
ipaddr = request.args['ipaddr'][0].strip()
actiontype = request.args['action'][0]
if not gameid.isalnum():
if not gameid.isalnum():
request.setResponseCode(500)
logger.log(logging.INFO,address+" Bad data "+gameid+" "+ipaddr)
logger.log(logging.INFO,
"%s Bad data %s %s",
address, gameid, ipaddr)
return "Bad data"
# this strips the region identifier from game IDs, not sure if this actually always accurate but limited testing suggests it is
# This strips the region identifier from game IDs, not sure if this
# actually always accurate but limited testing suggests it is
if len(gameid) > 3:
gameid = gameid[:-1]
if actiontype == 'ban':
dbconn.cursor().execute('insert into banned values(?,?)',(gameid,ipaddr))
responsedata = "Added gameid=%s, ipaddr=%s" % (gameid,ipaddr)
dbconn.cursor().execute(
'INSERT INTO banned VALUES(?,?)',
(gameid, ipaddr)
)
responsedata = "Added gameid=%s, ipaddr=%s" % (gameid, ipaddr)
else:
dbconn.cursor().execute('delete from banned where gameid=? and ipaddr=?',(gameid,ipaddr))
responsedata = "Removed gameid=%s, ipaddr=%s" % (gameid,ipaddr)
dbconn.cursor().execute(
'DELETE FROM banned WHERE gameid=? AND ipaddr=?',
(gameid, ipaddr)
)
responsedata = "Removed gameid=%s, ipaddr=%s" % (gameid, ipaddr)
dbconn.commit()
dbconn.close()
logger.log(logging.INFO,address+" "+responsedata)
logger.log(logging.INFO, "%s %s", address, responsedata)
request.setHeader("Content-Type", "text/html; charset=utf-8")
referer = request.getHeader('referer')
if not referer:
referer = "/banhammer"
request.setHeader("Location", referer)
request.setResponseCode(303)
return responsedata
def update_consolelist(self, request):
address = request.getClientIP()
dbconn = sqlite3.connect('gpcm.db')
@@ -155,22 +173,37 @@ class AdminPage(resource.Resource):
actiontype = request.args['action'][0]
if not macadr.isalnum():
request.setResponseCode(500)
logger.log(logging.INFO,address+" Bad data "+macadr+" ")
logger.log(logging.INFO, "%s Bad data %s", address, macadr)
return "Bad data"
if actiontype == 'add':
dbconn.cursor().execute('insert into pending values(?)',(macadr,))
dbconn.cursor().execute('insert into registered values(?)',(macadr,))
dbconn.cursor().execute(
'INSERT INTO pending VALUES(?)',
(macadr,)
)
dbconn.cursor().execute(
'INSERT INTO registered VALUES(?)',
(macadr,)
)
responsedata = "Added macadr=%s" % (macadr)
elif actiontype == 'activate':
dbconn.cursor().execute('insert into registered values(?)',(macadr,))
dbconn.cursor().execute(
'INSERT INTO registered VALUES(?)',
(macadr,)
)
responsedata = "Activated console belonging to %s" % (macadr)
else:
dbconn.cursor().execute('delete from pending where macadr=?',(macadr,))
dbconn.cursor().execute('delete from registered where macadr=?',(macadr,))
dbconn.cursor().execute(
'DELETE FROM pending WHERE macadr=?',
(macadr,)
)
dbconn.cursor().execute(
'DELETE FROM registered WHERE macadr=?',
(macadr,)
)
responsedata = "Removed macadr=%s" % (macadr)
dbconn.commit()
dbconn.close()
logger.log(logging.INFO,address+" "+responsedata)
logger.log(logging.INFO, "%s %s", address, responsedata)
request.setHeader("Content-Type", "text/html; charset=utf-8")
request.setHeader("Location", "/consoles")
referer = request.getHeader('referer')
@@ -185,55 +218,76 @@ class AdminPage(resource.Resource):
def render_banlist(self, request):
address = request.getClientIP()
dbconn = sqlite3.connect('gpcm.db')
logger.log(logging.INFO,address+" Viewed banlist")
responsedata = (""
'<a href="http://%20:%20@'+request.getHeader('host')+'">[CLICK HERE TO LOG OUT]</a>'
"<table border='1'>"
"<tr><td>gameid</td><td>ipAddr</td></tr>\r\n")
for row in dbconn.cursor().execute("select * from banned"):
logger.log(logging.INFO, "%s Viewed banlist", address)
responsedata = """
<a href="http://%%20:%%20@%s">[CLICK HERE TO LOG OUT]</a>
<table border='1'>
<tr>
<td>gameid</td>
<td>ipAddr</td>
</tr>""" % (request.getHeader('host'))
for row in dbconn.cursor().execute("SELECT * FROM banned"):
gameid = str(row[0])
ipaddr = str(row[1])
responsedata += ("<tr><td>"+gameid+"</td><td>"+ipaddr+"</td>"
"<td><form action='updatebanlist' method='POST'>"
"<input type='hidden' name='gameid' value='"+gameid+"'>"
"<input type='hidden' name='ipaddr' value='"+ipaddr+"'>"
"<input type='hidden' name='action' value='unban'>\r\n"
"<input type='submit' value='----- UNBAN -----'></form></td></tr>\r\n")
responsedata += "</table>"
# TODO: Use .format()/positional arguments
responsedata += """
<tr>
<td>%s</td>
<td>%s</td>
<td>
<form action='updatebanlist' method='POST'>
<input type='hidden' name='gameid' value='%s'>
<input type='hidden' name='ipaddr' value='%s'>
<input type='hidden' name='action' value='unban'>
<input type='submit' value='----- UNBAN -----'>
</form>
</td>
</tr>""" % (gameid, ipaddr, gameid, ipaddr)
responsedata += "</table>"
dbconn.close()
request.setHeader("Content-Type", "text/html; charset=utf-8")
return responsedata
def render_not_available(self, request):
request.setResponseCode(403)
request.setHeader('WWW-Authenticate', 'Basic realm="ALTWFC"')
request.write('No admin credentials set. Admin page is not available.')
def render_blacklist(self, request):
sqlstatement = (''
'select users.profileid,enabled,data,users.gameid,console,users.userid '
'from nas_logins '
'inner join users '
'on users.userid = nas_logins.userid '
'inner join ( '
' select max(profileid) newestpid, userid, gameid, devname '
' from users '
' group by userid,gameid) '
'ij on ij.userid = users.userid and '
'users.profileid = ij.newestpid '
'order by users.gameid '
'')
sqlstatement = """
SELECT users.profileid, enabled, data, users.gameid, console,
users.userid
FROM nas_logins
INNER JOIN users
ON users.userid = nas_logins.userid
INNER JOIN (
SELECT max(profileid) newestpid, userid, gameid, devname
FROM users '
GROUP BY userid, gameid
) ij
ON ij.userid = users.userid
AND users.profileid = ij.newestpid
ORDER BY users.gameid"""
dbconn = sqlite3.connect('gpcm.db')
banned_list = []
for row in dbconn.cursor().execute("SELECT * FROM BANNED"):
banned_list.append(str(row[0])+":"+str(row[1]))
responsedata = (""
'<a href="http://%20:%20@'+request.getHeader('host')+'">[CLICK HERE TO LOG OUT]</a>'
"<br><br>"
"<table border='1'>"
"<tr><td>ingamesn or devname</td><td>gameid</td>"
"<td>Enabled</td><td>newest dwc_pid</td>"
"<td>gsbrcd</td><td>userid</td><td>ipAddr</td></tr>\r\n")
responsedata = """
<a href="http://%%20:%%20@%s">[CLICK HERE TO LOG OUT]</a>
<br><br>
<table border='1'>"
<tr>
<td>ingamesn or devname</td>
<td>gameid</td>
<td>Enabled</td>
<td>newest dwc_pid</td>"
<td>gsbrcd</td>
<td>userid</td>
<td>ipAddr</td>
</tr>""" % request.getHeader('host')
for row in dbconn.cursor().execute(sqlstatement):
dwc_pid = str(row[0])
enabled = str(row[1])
@@ -256,31 +310,51 @@ class AdminPage(resource.Resource):
ingamesn = codecs.utf_16_le_decode(ingamesn)[0]
else:
ingamesn = '[NOT AVAILABLE]'
responsedata += "<tr>"
responsedata += "<td>"+ingamesn+"</td>"
responsedata += "<td>"+gameid+"</td>"
responsedata += "<td>"+enabled+"</td>"
responsedata += "<td>"+dwc_pid+"</td>"
responsedata += "<td>"+gsbrcd+"</td>"
responsedata += "<td>"+userid+"</td>"
responsedata += "<td>"+ipaddr+"</td>"
if gameid[:-1]+":"+ipaddr in banned_list:
responsedata += ("<td><form action='updatebanlist' method='POST'>"
"<input type='hidden' name='gameid' value='"+gameid+"'>"
"<input type='hidden' name='ipaddr' value='"+ipaddr+"'>"
"<input type='hidden' name='action' value='unban'>"
"<input type='submit' value='----- unban -----'></form></td></tr>")
responsedata += """
<tr>
<td>%s</td>
<td>%s</td>
<td>%s</td>
<td>%s</td>
<td>%s</td>
<td>%s</td>
<td>%s</td>
""" % (ingamesn,
gameid,
enabled,
dwc_pid,
gsbrcd,
userid,
ipaddr)
if gameid[:-1] + ":" + ipaddr in banned_list:
responsedata += """
<td>
<form action='updatebanlist' method='POST'>
<input type='hidden' name='gameid' value='%s'>
<input type='hidden' name='ipaddr' value='%s'>
<input type='hidden' name='action' value='unban'>
<input type='submit' value='----- unban -----'>
</form>
</td>
</tr>""" % (gameid, ipaddr)
else:
responsedata += ("<td><form action='updatebanlist' method='POST'>"
"<input type='hidden' name='gameid' value='"+gameid+"'>"
"<input type='hidden' name='ipaddr' value='"+ipaddr+"'>"
"<input type='hidden' name='action' value='ban'>"
"<input type='submit' value='Ban'></form></td></tr>")
responsedata += "</table>"
responsedata += """
<td>
<form action='updatebanlist' method='POST'>
<input type='hidden' name='gameid' value='%s'>
<input type='hidden' name='ipaddr' value='%s'>
<input type='hidden' name='action' value='ban'>
<input type='submit' value='Ban'>
</form>
</td>
</tr>
""" % (gameid, ipaddr)
responsedata += "</table>"
dbconn.close()
request.setHeader("Content-Type", "text/html; charset=utf-8")
return responsedata.encode('utf-8')
def enable_disable_user(self, request, enable=True):
address = request.getClientIP()
responsedata = ""
@@ -289,23 +363,31 @@ class AdminPage(resource.Resource):
ingamesn = request.args['ingamesn'][0]
if not userid.isdigit() or not gameid.isalnum():
logger.log(logging.INFO,address+" Bad data "+userid+" "+gameid)
logger.log(logging.INFO,
"%s Bad data %s %s",
address, userid, gameid)
return "Bad data"
dbconn = sqlite3.connect('gpcm.db')
if enable:
dbconn.cursor().execute('update users set enabled=1 '
'where gameid=? and userid=?',(gameid,userid))
dbconn.cursor().execute(
'UPDATE users SET enabled=1 '
'WHERE gameid=? AND userid=?',
(gameid, userid)
)
responsedata = "Enabled %s with gameid=%s, userid=%s" % \
(ingamesn,gameid,userid)
(ingamesn, gameid, userid)
else:
dbconn.cursor().execute('update users set enabled=0 '
'where gameid=? and userid=?',(gameid,userid))
dbconn.cursor().execute(
'UPDATE users SET enabled=0 '
'WHERE gameid=? AND userid=?',
(gameid, userid)
)
responsedata = "Disabled %s with gameid=%s, userid=%s" % \
(ingamesn,gameid,userid)
(ingamesn, gameid, userid)
dbconn.commit()
dbconn.close()
logger.log(logging.INFO,address+" "+responsedata)
logger.log(logging.INFO, "%s %s", address, responsedata)
request.setHeader("Content-Type", "text/html; charset=utf-8")
request.setHeader("Location", "/banhammer")
request.setResponseCode(303)
@@ -317,29 +399,44 @@ class AdminPage(resource.Resource):
active_list = []
for row in dbconn.cursor().execute("SELECT * FROM REGISTERED"):
active_list.append(str(row[0]))
logger.log(logging.INFO,address+" Viewed console list")
responsedata = (""
'<a href="http://%20:%20@'+request.getHeader('host')+'">[CLICK HERE TO LOG OUT]</a>'
logger.log(logging.INFO, "%s Viewed console list", address)
responsedata = (
'<a href="http://%20:%20@' + request.getHeader('host') +
'">[CLICK HERE TO LOG OUT]</a>'
"<form action='updateconsolelist' method='POST'>"
"macadr:<input type='text' name='macadr'>\r\n"
"<input type='hidden' name='action' value='add'>\r\n"
"<input type='submit' value='Register and activate console'></form>\r\n"
"<input type='submit' value='Register and activate console'>"
"</form>\r\n"
"<table border='1'>"
"<tr><td>macadr</td></tr>\r\n")
for row in dbconn.cursor().execute("select * from pending"):
"<tr><td>macadr</td></tr>\r\n"
)
for row in dbconn.cursor().execute("SELECT * FROM pending"):
macadr = str(row[0])
if macadr in active_list:
responsedata += ("<tr><td>"+macadr+"</td>"
"<td><form action='updateconsolelist' method='POST'>"
"<input type='hidden' name='macadr' value='"+macadr+"'>"
"<input type='hidden' name='action' value='remove'>\r\n"
"<input type='submit' value='Un-register console'></form></td></tr>\r\n")
responsedata += """
<tr>
<td>%s</td>
<td>
<form action='updateconsolelist' method='POST'>
<input type='hidden' name='macadr' value='%s'>
<input type='hidden' name='action' value='remove'>
<input type='submit' value='Un-register console'>
</form>
</td>
</tr>""" % (macadr, macadr)
else:
responsedata += ("<tr><td>"+macadr+"</td>"
"<td><form action='updateconsolelist' method='POST'>"
"<input type='hidden' name='macadr' value='"+macadr+"'>"
"<input type='hidden' name='action' value='activate'>\r\n"
"<input type='submit' value='Activate console'></form></td></tr>\r\n")
responsedata += """
<tr>
<td>%s</td>
<td>
<form action='updateconsolelist' method='POST'>
<input type='hidden' name='macadr' value='%s'>
<input type='hidden' name='action' value='activate'>
<input type='submit' value='Activate console'>
</form>
</td>
</tr>""" % (macadr, macadr)
responsedata += "</table>"
dbconn.close()
request.setHeader("Content-Type", "text/html; charset=utf-8")
@@ -351,7 +448,7 @@ class AdminPage(resource.Resource):
return ""
if not self.is_authorized(request):
return ""
title = None
response = ''
if request.path == "/banlist":
@@ -371,7 +468,7 @@ class AdminPage(resource.Resource):
return ""
if not self.is_authorized(request):
return ""
if request.path == "/updatebanlist":
return self.update_banlist(request)
if request.path == "/updateconsolelist":
@@ -379,17 +476,20 @@ class AdminPage(resource.Resource):
else:
return self.get_header() + self.get_footer()
port = 9009
class AdminPageServer(object):
def start(self):
site = server.Site(AdminPage(self))
reactor.listenTCP(port, site)
logger.log(logging.INFO, "Now listening for connections on port %d...", port)
logger.log(logging.INFO,
"Now listening for connections on port %d...",
port)
try:
if reactor.running == False:
if not reactor.running:
reactor.run(installSignalHandlers=0)
except ReactorAlreadyRunning:
pass
if __name__ == "__main__":
AdminPageServer().start()

123
altwfc.cfg Executable file
View File

@@ -0,0 +1,123 @@
# ALTWFC - Configuration File
[Config]
AlternativeConfig = OFF
AlternativeConfigFile = altwfc_nas.cfg
[StorageServer]
IP = 127.0.0.1
Port = 8000
LoggerName = StorageServer
LoggerFilename = storage_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[NasServer]
IP = 127.0.0.1
Port = 9000
LoggerName = NasServer
LoggerFilename = nas_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[InternalStatsServer]
IP = 127.0.0.1
Port = 9001
LoggerName = InternalStatsServer
LoggerFilename = internal_stats_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[GameStatsServerHttp]
IP = 127.0.0.1
Port = 9002
LoggerName = GameStatsServerHttp
LoggerFilename = gamestats_server_http.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[AdminPage]
IP = 127.0.0.1
Port = 9009
LoggerName = AdminPage
LoggerFilename = admin_page.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[RegisterPage]
IP = 127.0.0.1
Port = 9998
LoggerName = RegisterPage
LoggerFilename = register_page.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[GameSpyManager]
# GamespyBackendServer
IP = 127.0.0.1
Port = 27500
LoggerName = GamespyBackendServer
LoggerFilename = gamespy_backend_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[GameSpyQRServer]
IP = 0.0.0.0
Port = 27900
LoggerName = GameSpyQRServer
LoggerFilename = gamespy_qr_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[GameSpyNatNegServer]
IP = 0.0.0.0
Port = 27901
LoggerName = GameSpyNatNegServer
LoggerFilename = gamespy_natneg_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[GameSpyServerBrowserServer]
IP = 0.0.0.0
Port = 28910
LoggerName = GameSpyServerBrowserServer
LoggerFilename = gamespy_server_browser_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[GameSpyProfileServer]
IP = 0.0.0.0
Port = 29900
LoggerName = GameSpyProfileServer
LoggerFilename = gamespy_profile_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[GameSpyPlayerSearchServer]
IP = 0.0.0.0
Port = 29901
LoggerName = GameSpyPlayerSearchServer
LoggerFilename = gamespy_player_search_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[GameSpyGamestatsServer]
IP = 0.0.0.0
Port = 29920
LoggerName = GameSpyGamestatsServer
LoggerFilename = gamespy_gamestats_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON

119
altwfc_nas.cfg Normal file
View File

@@ -0,0 +1,119 @@
# ALTWFC - Alternative Configuration File
[StorageServer]
IP = 127.0.0.1
Port = 8000
LoggerName = StorageServer
LoggerFilename = storage_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[NasServer]
IP = 127.0.0.1
Port = 80
LoggerName = NasServer
LoggerFilename = nas_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[InternalStatsServer]
IP = 127.0.0.1
Port = 9001
LoggerName = InternalStatsServer
LoggerFilename = internal_stats_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[GameStatsServerHttp]
IP = 127.0.0.1
Port = 9002
LoggerName = GameStatsServerHttp
LoggerFilename = gamestats_server_http.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[AdminPage]
IP = 127.0.0.1
Port = 9009
LoggerName = AdminPage
LoggerFilename = admin_page.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[RegisterPage]
IP = 127.0.0.1
Port = 9998
LoggerName = RegisterPage
LoggerFilename = register_page.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[GameSpyManager]
# GamespyBackendServer
IP = 127.0.0.1
Port = 27500
LoggerName = GamespyBackendServer
LoggerFilename = gamespy_backend_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[GameSpyQRServer]
IP = 0.0.0.0
Port = 27900
LoggerName = GameSpyQRServer
LoggerFilename = gamespy_qr_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[GameSpyNatNegServer]
IP = 0.0.0.0
Port = 27901
LoggerName = GameSpyNatNegServer
LoggerFilename = gamespy_natneg_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[GameSpyServerBrowserServer]
IP = 0.0.0.0
Port = 28910
LoggerName = GameSpyServerBrowserServer
LoggerFilename = gamespy_server_browser_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[GameSpyProfileServer]
IP = 0.0.0.0
Port = 29900
LoggerName = GameSpyProfileServer
LoggerFilename = gamespy_profile_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[GameSpyPlayerSearchServer]
IP = 0.0.0.0
Port = 29901
LoggerName = GameSpyPlayerSearchServer
LoggerFilename = gamespy_player_search_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON
[GameSpyGamestatsServer]
IP = 0.0.0.0
Port = 29920
LoggerName = GameSpyGamestatsServer
LoggerFilename = gamespy_gamestats_server.log
LoggerLevel = -1
LoggerOutputConsole = ON
LoggerOutputFile = ON

77
dwc_config.py Executable file
View File

@@ -0,0 +1,77 @@
"""DWC Network Server Emulator
Copyright (C) 2014 SMTDDR
Copyright (C) 2014 kyle95wm
Copyright (C) 2014 AdmiralCurtiss
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Configuration module.
"""
try:
# Python 2
import ConfigParser
except ImportError:
# Python 3
import configparser as ConfigParser
import other.utils as utils
def get_config_filename(filename='altwfc.cfg'):
"""Return the config filename that will be used."""
try:
config = ConfigParser.RawConfigParser(allow_no_value=True)
config.read(filename)
if config.getboolean('Config', 'AlternativeConfig'):
return config.get('Config', 'AlternativeConfigFile')
except Exception as e:
pass
return filename
def get_ip_port(section, filename='altwfc.cfg'):
"""Return a tuple (IP, Port) of the corresponding section."""
config = ConfigParser.RawConfigParser(allow_no_value=True)
config.read(get_config_filename(filename))
return (config.get(section, 'IP'), config.getint(section, 'Port'))
def get_ip(section, filename='altwfc.cfg'):
"""Return the IP of the corresponding section."""
config = ConfigParser.RawConfigParser(allow_no_value=True)
config.read(get_config_filename(filename))
return config.get(section, 'IP')
def get_port(section, filename='altwfc.cfg'):
"""Return the port of the corresponding section."""
config = ConfigParser.RawConfigParser(allow_no_value=True)
config.read(get_config_filename(filename))
return config.getint(section, 'Port')
def get_logger(section, filename='altwfc.cfg'):
"""Return the logger of the corresponding section."""
config = ConfigParser.RawConfigParser(allow_no_value=True)
config.read(get_config_filename(filename))
return utils.create_logger(
config.get(section, 'LoggerName'),
config.get(section, 'LoggerFilename'),
config.getint(section, 'LoggerLevel'),
config.getboolean(section, 'LoggerOutputConsole'),
config.getboolean(section, 'LoggerOutputFile')
)

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
@@ -24,7 +24,13 @@
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<PtvsTargetsFile>$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets</PtvsTargetsFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="admin_page_server.py" />
<Compile Include="dwc_config.py" />
<Compile Include="gamespy_backend_server.py" />
<Compile Include="gamespy_gamestats_server.py" />
<Compile Include="gamespy_natneg_server.py" />
@@ -32,9 +38,11 @@
<Compile Include="gamespy_profile_server.py" />
<Compile Include="gamespy_qr_server.py" />
<Compile Include="gamespy_server_browser_server.py" />
<Compile Include="gamestats_server_http.py" />
<Compile Include="internal_stats_server.py" />
<Compile Include="master_server.py" />
<Compile Include="nas_server.py" />
<Compile Include="register_page.py" />
<Compile Include="storage_server.py" />
<Compile Include="gamespy\gs_database.py" />
<Compile Include="gamespy\gs_query.py" />
@@ -56,5 +64,6 @@
<Folder Include="www\gamestats.gs.nintendowifi.net\" />
<Folder Include="www\gamestats.gs.nintendowifi.net\public_html" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.targets" />
<Import Project="$(PtvsTargetsFile)" Condition="Exists($(PtvsTargetsFile))" />
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.targets" Condition="!Exists($(PtvsTargetsFile))" />
</Project>

View File

@@ -1,6 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 11.00
# Visual Studio 2010
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.23107.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "dwc_network_server_emulator", "dwc_network_server_emulator.pyproj", "{1A9E8A26-6CEA-4E78-835E-B043356CE360}"
EndProject
Global

View File

@@ -1,20 +1,23 @@
# DWC Network Server Emulator
# Copyright (C) 2014 polaris-
# Copyright (C) 2014 ToadKing
# Copyright (C) 2014 AdmiralCurtiss
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""DWC Network Server Emulator
Copyright (C) 2014 polaris-
Copyright (C) 2014 ToadKing
Copyright (C) 2014 AdmiralCurtiss
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import sqlite3
import hashlib
@@ -33,7 +36,9 @@ logger_output_to_console = True
logger_output_to_file = True
logger_name = "GamespyDatabase"
logger_filename = "gamespy_database.log"
logger = utils.create_logger(logger_name, logger_filename, -1, logger_output_to_console, logger_output_to_file)
logger = utils.create_logger(logger_name, logger_filename, -1,
logger_output_to_console, logger_output_to_file)
class Transaction(object):
def __init__(self, connection):
@@ -51,7 +56,8 @@ class Transaction(object):
def _executeAndMeasure(self, cursor, statement, parameters):
logTransactionId = utils.generate_random_str(8)
logger.log(SQL_LOGLEVEL, "[%s] STARTING: " % logTransactionId + statement.replace('?', '%s') % parameters)
logger.log(SQL_LOGLEVEL, "[%s] STARTING: " % logTransactionId +
statement.replace('?', '%s') % parameters)
timeStart = time.time()
clockStart = time.clock()
@@ -62,10 +68,16 @@ class Transaction(object):
timeEnd = time.time()
timeDiff = timeEnd - timeStart
logger.log(SQL_LOGLEVEL, "[%s] DONE: Took %s real time / %s processor time", logTransactionId, timeDiff, clockEnd - clockStart)
logger.log(SQL_LOGLEVEL,
"[%s] DONE: Took %s real time / %s processor time",
logTransactionId, timeDiff, clockEnd - clockStart)
if timeDiff > 1.0:
logger.log(logging.WARNING, "[%s] WARNING: SQL Statement took %s seconds!", logTransactionId, timeDiff)
logger.log(logging.WARNING, "[%s] " % logTransactionId + statement.replace('?', '%s') % parameters)
logger.log(logging.WARNING,
"[%s] WARNING: SQL Statement took %s seconds!",
logTransactionId, timeDiff)
logger.log(logging.WARNING,
"[%s] " % logTransactionId +
statement.replace('?', '%s') % parameters)
return
def queryall(self, statement, parameters=()):
@@ -88,51 +100,90 @@ class Transaction(object):
self.databaseAltered = True
return
class GamespyDatabase(object):
def __init__(self, filename='gpcm.db'):
self.conn = sqlite3.connect(filename, timeout=10.0)
self.conn.row_factory = sqlite3.Row
#self.initialize_database()
# self.initialize_database()
def __del__(self):
self.close()
def close(self):
if self.conn != None:
if self.conn is not None:
self.conn.close()
self.conn = None
def initialize_database(self):
with Transaction(self.conn) as tx:
# I highly doubt having everything in a database be of the type TEXT is a good practice,
# but I'm not good with databases and I'm not 100% positive that, for instance, that all
# user id's will be ints, or all passwords will be ints, etc, despite not seeing any
# evidence yet to say otherwise as far as Nintendo DS games go.
# I highly doubt having everything in a database be of the type
# TEXT is a good practice, but I'm not good with databases and
# I'm not 100% positive that, for instance, that all user id's
# will be ints, or all passwords will be ints, etc, despite not
# seeing any evidence yet to say otherwise as far as Nintendo
# DS games go.
tx.nonquery("CREATE TABLE IF NOT EXISTS users (profileid INT, userid TEXT, password TEXT, gsbrcd TEXT, email TEXT, uniquenick TEXT, pid TEXT, lon TEXT, lat TEXT, loc TEXT, firstname TEXT, lastname TEXT, stat TEXT, partnerid TEXT, console INT, csnum TEXT, cfc TEXT, bssid TEXT, devname BLOB, birth TEXT, gameid TEXT, enabled INT, zipcode TEXT, aim TEXT)")
tx.nonquery("CREATE TABLE IF NOT EXISTS sessions (session TEXT, profileid INT, loginticket TEXT)")
tx.nonquery("CREATE TABLE IF NOT EXISTS buddies (userProfileId INT, buddyProfileId INT, time INT, status INT, notified INT, gameid TEXT, blocked INT)")
tx.nonquery("CREATE TABLE IF NOT EXISTS pending_messages (sourceid INT, targetid INT, msg TEXT)")
tx.nonquery("CREATE TABLE IF NOT EXISTS gamestat_profile (profileid INT, dindex TEXT, ptype TEXT, data TEXT)")
tx.nonquery("CREATE TABLE IF NOT EXISTS gameinfo (profileid INT, dindex TEXT, ptype TEXT, data TEXT)")
tx.nonquery("CREATE TABLE IF NOT EXISTS nas_logins (userid TEXT, authtoken TEXT, data TEXT)")
tx.nonquery("CREATE TABLE IF NOT EXISTS banned (gameid TEXT, ipaddr TEXT)")
tx.nonquery("CREATE TABLE IF NOT EXISTS users"
" (profileid INT, userid TEXT, password TEXT,"
" gsbrcd TEXT, email TEXT, uniquenick TEXT,"
" pid TEXT, lon TEXT, lat TEXT, loc TEXT,"
" firstname TEXT, lastname TEXT, stat TEXT,"
" partnerid TEXT, console INT, csnum TEXT,"
" cfc TEXT, bssid TEXT, devname BLOB, birth TEXT,"
" gameid TEXT, enabled INT, zipcode TEXT, aim TEXT)")
tx.nonquery("CREATE TABLE IF NOT EXISTS sessions"
" (session TEXT, profileid INT, loginticket TEXT)")
tx.nonquery("CREATE TABLE IF NOT EXISTS buddies"
" (userProfileId INT, buddyProfileId INT, time INT,"
" status INT, notified INT, gameid TEXT,"
" blocked INT)")
tx.nonquery("CREATE TABLE IF NOT EXISTS pending_messages"
" (sourceid INT, targetid INT, msg TEXT)")
tx.nonquery("CREATE TABLE IF NOT EXISTS gamestat_profile"
" (profileid INT, dindex TEXT, ptype TEXT,"
" data TEXT)")
tx.nonquery("CREATE TABLE IF NOT EXISTS gameinfo"
" (profileid INT, dindex TEXT, ptype TEXT,"
" data TEXT)")
tx.nonquery("CREATE TABLE IF NOT EXISTS nas_logins"
" (userid TEXT, authtoken TEXT, data TEXT)")
tx.nonquery("CREATE TABLE IF NOT EXISTS banned"
" (gameid TEXT, ipaddr TEXT)")
tx.nonquery("CREATE TABLE IF NOT EXISTS pending (macadr TEXT)")
tx.nonquery("CREATE TABLE IF NOT EXISTS registered (macadr TEXT)")
# Create some indexes for performance.
tx.nonquery("CREATE UNIQUE INDEX IF NOT EXISTS gamestatprofile_triple on gamestat_profile(profileid,dindex,ptype)")
tx.nonquery("CREATE UNIQUE INDEX IF NOT EXISTS users_profileid_idx ON users (profileid)")
tx.nonquery("CREATE INDEX IF NOT EXISTS users_userid_idx ON users (userid)")
tx.nonquery("CREATE INDEX IF NOT EXISTS pending_messages_targetid_idx ON pending_messages (targetid)")
tx.nonquery("CREATE UNIQUE INDEX IF NOT EXISTS sessions_session_idx ON sessions (session)")
tx.nonquery("CREATE INDEX IF NOT EXISTS sessions_loginticket_idx ON sessions (loginticket)")
tx.nonquery("CREATE INDEX IF NOT EXISTS sessions_profileid_idx ON sessions (profileid)")
tx.nonquery("CREATE UNIQUE INDEX IF NOT EXISTS nas_logins_authtoken_idx ON nas_logins (authtoken)")
tx.nonquery("CREATE INDEX IF NOT EXISTS nas_logins_userid_idx ON nas_logins (userid)")
tx.nonquery("CREATE INDEX IF NOT EXISTS buddies_userProfileId_idx ON buddies (userProfileId)")
tx.nonquery("CREATE INDEX IF NOT EXISTS buddies_buddyProfileId_idx ON buddies (buddyProfileId)")
tx.nonquery("CREATE INDEX IF NOT EXISTS gamestat_profile_profileid_idx ON gamestat_profile (profileid)")
tx.nonquery("CREATE UNIQUE INDEX IF NOT EXISTS"
" gamestatprofile_triple"
" ON gamestat_profile(profileid,dindex,ptype)")
tx.nonquery("CREATE UNIQUE INDEX IF NOT EXISTS"
" users_profileid_idx ON users (profileid)")
tx.nonquery("CREATE INDEX IF NOT EXISTS"
" users_userid_idx ON users (userid)")
tx.nonquery("CREATE INDEX IF NOT EXISTS"
" pending_messages_targetid_idx"
" ON pending_messages (targetid)")
tx.nonquery("CREATE UNIQUE INDEX IF NOT EXISTS"
" sessions_session_idx ON sessions (session)")
tx.nonquery("CREATE INDEX IF NOT EXISTS"
" sessions_loginticket_idx ON sessions (loginticket)")
tx.nonquery("CREATE INDEX IF NOT EXISTS"
" sessions_profileid_idx ON sessions (profileid)")
tx.nonquery("CREATE UNIQUE INDEX IF NOT EXISTS"
" nas_logins_authtoken_idx ON nas_logins (authtoken)")
tx.nonquery("CREATE INDEX IF NOT EXISTS"
" nas_logins_userid_idx ON nas_logins (userid)")
tx.nonquery("CREATE INDEX IF NOT EXISTS"
" buddies_userProfileId_idx"
" ON buddies (userProfileId)")
tx.nonquery("CREATE INDEX IF NOT EXISTS"
" buddies_buddyProfileId_idx"
" ON buddies (buddyProfileId)")
tx.nonquery("CREATE INDEX IF NOT EXISTS"
" gamestat_profile_profileid_idx"
" ON gamestat_profile (profileid)")
def get_dict(self, row):
if not row:
@@ -142,23 +193,28 @@ class GamespyDatabase(object):
# User functions
def get_next_free_profileid(self):
# TODO: Make profile ids start at 1 for each game?
"""TODO: Make profile ids start at 1 for each game?
# TODO: This leads to a race condition if two users try to create accounts at the same time.
# Instead, it's better to create a new row and return the sqlite ROWID instead.
TODO: This leads to a race condition if two users try to create
accounts at the same time. Instead, it's better to create a new row
and return the sqlite ROWID instead.
"""
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT max(profileid) AS m FROM users")
r = self.get_dict(row)
profileid = 1 # Cannot be 0 or else it freezes the game.
if r != None and r['m'] != None:
profileid = 1 # Cannot be 0 or else it freezes the game.
if r is not None and r['m'] is not None:
profileid = int(r['m']) + 1
return profileid
def check_user_exists(self, userid, gsbrcd):
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT COUNT(*) FROM users WHERE userid = ? AND gsbrcd = ?", (userid, gsbrcd))
row = tx.queryone(
"SELECT COUNT(*) FROM users WHERE userid = ? AND gsbrcd = ?",
(userid, gsbrcd)
)
count = int(row[0])
valid_user = False # Default, user doesn't exist
@@ -169,13 +225,19 @@ class GamespyDatabase(object):
def check_user_enabled(self, userid, gsbrcd):
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT enabled FROM users WHERE userid = ? AND gsbrcd = ?", (userid, gsbrcd))
row = tx.queryone(
"SELECT enabled FROM users WHERE userid = ? AND gsbrcd = ?",
(userid, gsbrcd)
)
enabled = int(row[0])
return enabled > 0
def check_profile_exists(self, profileid):
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT COUNT(*) FROM users WHERE profileid = ?", (profileid,))
row = tx.queryone(
"SELECT COUNT(*) FROM users WHERE profileid = ?",
(profileid,)
)
count = int(row[0])
valid_profile = False # Default, user doesn't exist
@@ -188,36 +250,44 @@ class GamespyDatabase(object):
profile = {}
if profileid != 0:
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT * FROM users WHERE profileid = ?", (profileid,))
row = tx.queryone(
"SELECT * FROM users WHERE profileid = ?",
(profileid,)
)
profile = self.get_dict(row)
return profile
def perform_login(self, userid, password, gsbrcd):
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT * FROM users WHERE userid = ? and gsbrcd = ?", (userid, gsbrcd))
row = tx.queryone(
"SELECT * FROM users WHERE userid = ? and gsbrcd = ?",
(userid, gsbrcd)
)
r = self.get_dict(row)
profileid = None # Default, user doesn't exist
if r != None:
#md5 = hashlib.md5()
#md5.update(password)
if r is not None:
# md5 = hashlib.md5()
# md5.update(password)
#if r['password'] == md5.hexdigest():
# profileid = r['profileid'] # Valid password
# if r['password'] == md5.hexdigest():
# profileid = r['profileid'] # Valid password
if r['enabled'] == 1 and r['gsbrcd'] == gsbrcd:
profileid = r['profileid'] # Valid password
return profileid
def create_user(self, userid, password, email, uniquenick, gsbrcd, console, csnum, cfc, bssid, devname, birth, gameid, macadr):
def create_user(self, userid, password, email, uniquenick, gsbrcd,
console, csnum, cfc, bssid, devname, birth, gameid,
macadr):
if self.check_user_exists(userid, gsbrcd) == 0:
profileid = self.get_next_free_profileid()
pid = "11" # Always 11??? Is this important? Not to be confused with dwc_pid.
# The three games I found it in (Tetris DS, Advance Wars - Days of Ruin, and
# Animal Crossing: Wild World) all use \pid\11.
# Always 11??? Is this important? Not to be confused with dwc_pid.
# The three games I found it in (Tetris DS, Advance Wars - Days of
# Ruin, and Animal Crossing: Wild World) all use \pid\11.
pid = "11"
lon = "0.000000" # Always 0.000000?
lat = "0.000000" # Always 0.000000?
loc = ""
@@ -231,19 +301,26 @@ class GamespyDatabase(object):
# Hash password before entering it into the database.
# For now I'm using a very simple MD5 hash.
# TODO: Replace with something stronger later, although it's overkill for the NDS.
# TODO: Replace with something stronger later, although it's
# overkill for the NDS.
md5 = hashlib.md5()
md5.update(password)
password = md5.hexdigest()
with Transaction(self.conn) as tx:
q = "INSERT INTO users VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
tx.nonquery(q, (profileid, str(userid), password, gsbrcd, email, uniquenick, pid, lon, lat, loc, firstname, lastname, stat, partnerid, console, csnum, cfc, bssid, devname, birth, gameid, enabled, zipcode, aim))
q = "INSERT INTO users VALUES" \
" (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
tx.nonquery(q, (profileid, str(userid), password, gsbrcd,
email, uniquenick, pid, lon, lat, loc,
firstname, lastname, stat, partnerid,
console, csnum, cfc, bssid, devname, birth,
gameid, enabled, zipcode, aim))
return profileid
return None
def import_user(self, profileid, uniquenick, firstname, lastname, email, gsbrcd, gameid, console):
def import_user(self, profileid, uniquenick, firstname, lastname, email,
gsbrcd, gameid, console):
if self.check_profile_exists(profileid) == 0:
pid = "11"
lon = "0.000000"
@@ -265,8 +342,13 @@ class GamespyDatabase(object):
enabled = 1
with Transaction(self.conn) as tx:
q = "INSERT INTO users VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
tx.nonquery(q, (profileid, str(userid), password, gsbrcd, email, uniquenick, pid, lon, lat, loc, firstname, lastname, stat, partnerid, console, csnum, cfc, bssid, devname, birth, gameid, enabled, zipcode, aim))
q = "INSERT INTO users VALUES" \
" (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
tx.nonquery(q, (profileid, str(userid), password, gsbrcd,
email, uniquenick, pid, lon, lat, loc,
firstname, lastname, stat, partnerid,
console, csnum, cfc, bssid, devname, birth,
gameid, enabled, zipcode, aim))
return profileid
@@ -274,51 +356,57 @@ class GamespyDatabase(object):
with Transaction(self.conn) as tx:
rows = tx.queryall("SELECT * FROM users")
users = []
for row in rows:
users.append(self.get_dict(row))
return users
return [self.get_dict(row) for row in rows]
def save_pending_message(self, sourceid, targetid, msg):
with Transaction(self.conn) as tx:
tx.nonquery("INSERT INTO pending_messages VALUES (?,?,?)", (sourceid, targetid, msg))
tx.nonquery("INSERT INTO pending_messages VALUES (?,?,?)",
(sourceid, targetid, msg))
def get_pending_messages(self, profileid):
with Transaction(self.conn) as tx:
rows = tx.queryall("SELECT * FROM pending_messages WHERE targetid = ?", (profileid,))
rows = tx.queryall(
"SELECT * FROM pending_messages WHERE targetid = ?",
(profileid,)
)
messages = []
for row in rows:
messages.append(self.get_dict(row))
return messages
return [self.get_dict(row) for row in rows]
def update_profile(self, profileid, field):
# Found profile id associated with session key.
# Start replacing each field one by one.
# TODO: Optimize this so it's done all in one update.
# FIXME: Possible security issue due to embedding an unsanitized string directly into the statement.
"""Found profile id associated with session key.
Start replacing each field one by one.
TODO: Optimize this so it's done all in one update.
FIXME: Possible security issue due to embedding an unsanitized
string directly into the statement.
"""
with Transaction(self.conn) as tx:
q = "UPDATE users SET \"%s\" = ? WHERE profileid = ?"
tx.nonquery(q % field[0], (field[1], profileid))
# Session functions
# TODO: Cache session keys so we don't have to query the database every time we get a profile id.
# TODO: Cache session keys so we don't have to query the database every
# time we get a profile id.
def get_profileid_from_session_key(self, session_key):
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT profileid FROM sessions WHERE session = ?", (session_key,))
row = tx.queryone(
"SELECT profileid FROM sessions WHERE session = ?",
(session_key,)
)
r = self.get_dict(row)
profileid = -1 # Default, invalid session key
if r != None:
if r is not None:
profileid = r['profileid']
return profileid
def get_profileid_from_loginticket(self, loginticket):
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT profileid FROM sessions WHERE loginticket = ?", (loginticket,))
row = tx.queryone(
"SELECT profileid FROM sessions WHERE loginticket = ?",
(loginticket,)
)
profileid = -1
if row:
@@ -332,28 +420,40 @@ class GamespyDatabase(object):
profile = {}
if profileid != 0:
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT profileid FROM sessions WHERE session = ?", (session_key,))
row = tx.queryone(
"SELECT profileid FROM sessions WHERE session = ?",
(session_key,)
)
profile = self.get_dict(row)
return profile
def generate_session_key(self, min_size):
# TODO: There's probably a better way to do this.
# The point is preventing duplicate session keys.
"""Generate session key.
TODO: There's probably a better way to do this.
The point is preventing duplicate session keys.
"""
while True:
with Transaction(self.conn) as tx:
session_key = utils.generate_random_number_str(min_size)
row = tx.queryone("SELECT COUNT(*) FROM sessions WHERE session = ?", (session_key,))
row = tx.queryone(
"SELECT COUNT(*) FROM sessions WHERE session = ?",
(session_key,)
)
count = int(row[0])
if count == 0:
return session_key
def delete_session(self, profileid):
with Transaction(self.conn) as tx:
tx.nonquery("DELETE FROM sessions WHERE profileid = ?", (profileid,))
tx.nonquery(
"DELETE FROM sessions WHERE profileid = ?",
(profileid,)
)
def create_session(self, profileid, loginticket):
if profileid != None and self.check_profile_exists(profileid) == False:
if profileid is not None and not self.check_profile_exists(profileid):
return None
# Remove any old sessions associated with this user id
@@ -362,87 +462,119 @@ class GamespyDatabase(object):
# Create new session
session_key = self.generate_session_key(8)
with Transaction(self.conn) as tx:
tx.nonquery("INSERT INTO sessions VALUES (?, ?, ?)", (session_key, profileid, loginticket))
tx.nonquery(
"INSERT INTO sessions VALUES (?, ?, ?)",
(session_key, profileid, loginticket)
)
return session_key
def get_session_list(self, profileid=None):
sessions = []
with Transaction(self.conn) as tx:
if profileid != None:
r = tx.queryall("SELECT * FROM sessions WHERE profileid = ?", (profileid,))
if profileid is not None:
r = tx.queryall(
"SELECT * FROM sessions WHERE profileid = ?",
(profileid,)
)
else:
r = tx.queryall("SELECT * FROM sessions")
for row in r:
sessions.append(self.get_dict(row))
return sessions
return [self.get_dict(row) for row in r]
# nas server functions
def get_nas_login(self, authtoken):
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT data FROM nas_logins WHERE authtoken = ?", (authtoken,))
row = tx.queryone(
"SELECT data FROM nas_logins WHERE authtoken = ?",
(authtoken,)
)
r = self.get_dict(row)
if r == None:
if r is None:
return None
else:
return json.loads(r["data"])
def get_nas_login_from_userid(self, userid):
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT data FROM nas_logins WHERE userid = ?", (userid,))
row = tx.queryone(
"SELECT data FROM nas_logins WHERE userid = ?",
(userid,)
)
r = self.get_dict(row)
if r == None:
if r is None:
return None
else:
return json.loads(r["data"])
def is_banned(self,postdata):
def is_banned(self, postdata):
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT COUNT(*) FROM banned WHERE gameid = ? AND ipaddr = ?",(postdata['gamecd'][:-1],postdata['ipaddr']))
row = tx.queryone(
"SELECT COUNT(*) FROM banned WHERE gameid = ? AND ipaddr = ?",
(postdata['gamecd'][:-1], postdata['ipaddr'])
)
return int(row[0]) > 0
def pending(self, postdata):
with Transaction(self.conn) as tx:
row = tx.queryone(
"SELECT COUNT(*) FROM pending WHERE macadr = ?",
(postdata['macadr'],)
)
return int(row[0]) > 0
def pending(self,postdata):
def registered(self, postdata):
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT COUNT(*) FROM pending WHERE macadr = ?",(postdata['macadr'],))
return int(row[0]) > 0
def registered(self,postdata):
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT COUNT(*) FROM registered WHERE macadr = ?",(postdata['macadr'],))
row = tx.queryone(
"SELECT COUNT(*) FROM registered WHERE macadr = ?",
(postdata['macadr'],)
)
return int(row[0]) > 0
def get_next_available_userid(self):
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT max(userid) AS maxuser FROM users")
r = self.get_dict(row)
if r == None or r['maxuser'] == None:
return '0000000000002'#Because all zeroes means Dolphin. Don't wanna get confused during debugging later.
if r is None or r['maxuser'] is None:
# Because all zeroes means Dolphin. Don't wanna get confused
# during debugging later.
return '0000000000002'
else:
userid = str(int(r['maxuser']) + 1)
while len(userid) < 13:
userid = "0"+userid
userid = "0" + userid
return userid
def generate_authtoken(self, userid, data):
# Since the auth token passed back to the game will be random, we can make it small enough that there
# should never be a crash due to the size of the token.
# ^ real authtoken is 80 + 3 bytes though and I want to figure out what's causing the 52200
# so I'm matching everything as closely as possible to the real thing
"""Generate authentication token.
Since the auth token passed back to the game will be random, we can
make it small enough that there should never be a crash due to the
size of the token.
^ real authtoken is 80 + 3 bytes though and I want to figure out
what's causing the 52200 so I'm matching everything as closely as
possible to the real thing.
"""
size = 80
# TODO: Another one of those questionable dupe-preventations
while True:
with Transaction(self.conn) as tx:
authtoken = "NDS" + utils.generate_random_str(size)
row = tx.queryone("SELECT COUNT(*) FROM nas_logins WHERE authtoken = ?", (authtoken,))
row = tx.queryone(
"SELECT COUNT(*) FROM nas_logins WHERE authtoken = ?",
(authtoken,)
)
count = int(row[0])
if count == 0:
break
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT * FROM nas_logins WHERE userid = ?", (userid,))
row = tx.queryone(
"SELECT * FROM nas_logins WHERE userid = ?",
(userid,)
)
r = self.get_dict(row)
if "devname" in data:
@@ -453,13 +585,19 @@ class GamespyDatabase(object):
data = json.dumps(data)
with Transaction(self.conn) as tx:
if r == None: # no row, add it
tx.nonquery("INSERT INTO nas_logins VALUES (?, ?, ?)", (userid, authtoken, data))
if r is None: # no row, add it
tx.nonquery(
"INSERT INTO nas_logins VALUES (?, ?, ?)",
(userid, authtoken, data)
)
else:
tx.nonquery("UPDATE nas_logins SET authtoken = ?, data = ? WHERE userid = ?", (authtoken, data, userid))
tx.nonquery(
"UPDATE nas_logins SET authtoken = ?, data = ?"
" WHERE userid = ?",
(authtoken, data, userid)
)
return authtoken
# Buddy functions
def add_buddy(self, userProfileId, buddyProfileId):
@@ -467,88 +605,130 @@ class GamespyDatabase(object):
# status == 0 -> not authorized
with Transaction(self.conn) as tx:
tx.nonquery("INSERT INTO buddies VALUES (?, ?, ?, ?, ?, ?, ?)", (userProfileId, buddyProfileId, now, 0, 0, "", 0))
tx.nonquery(
"INSERT INTO buddies VALUES (?, ?, ?, ?, ?, ?, ?)",
(userProfileId, buddyProfileId, now, 0, 0, "", 0)
)
def auth_buddy(self, userProfileId, buddyProfileId):
# status == 1 -> authorized
with Transaction(self.conn) as tx:
tx.nonquery("UPDATE buddies SET status = ? WHERE userProfileId = ? AND buddyProfileId = ?", (1, userProfileId, buddyProfileId))
tx.nonquery(
"UPDATE buddies SET status = ?"
" WHERE userProfileId = ? AND buddyProfileId = ?",
(1, userProfileId, buddyProfileId)
)
def block_buddy(self, userProfileId, buddyProfileId):
with Transaction(self.conn) as tx:
tx.nonquery("UPDATE buddies SET blocked = ? WHERE userProfileId = ? AND buddyProfileId = ?", (1, userProfileId, buddyProfileId))
tx.nonquery(
"UPDATE buddies SET blocked = ?"
" WHERE userProfileId = ? AND buddyProfileId = ?",
(1, userProfileId, buddyProfileId)
)
def unblock_buddy(self, userProfileId, buddyProfileId):
with Transaction(self.conn) as tx:
tx.nonquery("UPDATE buddies SET blocked = ? WHERE userProfileId = ? AND buddyProfileId = ?", (0, userProfileId, buddyProfileId))
tx.nonquery(
"UPDATE buddies SET blocked = ?"
" WHERE userProfileId = ? AND buddyProfileId = ?",
(0, userProfileId, buddyProfileId)
)
def get_buddy(self, userProfileId, buddyProfileId):
profile = {}
if userProfileId != 0 and buddyProfileId != 0:
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT * FROM buddies WHERE userProfileId = ? AND buddyProfileId = ?", (userProfileId, buddyProfileId))
profile = self.get_dict(row)
return profile
row = tx.queryone(
"SELECT * FROM buddies"
" WHERE userProfileId = ? AND buddyProfileId = ?",
(userProfileId, buddyProfileId)
)
return self.get_dict(row)
return {}
def delete_buddy(self, userProfileId, buddyProfileId):
with Transaction(self.conn) as tx:
tx.nonquery("DELETE FROM buddies WHERE userProfileId = ? AND buddyProfileId = ?", (userProfileId, buddyProfileId))
tx.nonquery(
"DELETE FROM buddies"
" WHERE userProfileId = ? AND buddyProfileId = ?",
(userProfileId, buddyProfileId)
)
def get_buddy_list(self, userProfileId):
with Transaction(self.conn) as tx:
rows = tx.queryall("SELECT * FROM buddies WHERE userProfileId = ? AND blocked = 0", (userProfileId,))
rows = tx.queryall(
"SELECT * FROM buddies"
" WHERE userProfileId = ? AND blocked = 0",
(userProfileId,)
)
users = []
for row in rows:
users.append(self.get_dict(row))
return users
return [self.get_dict(row) for row in rows]
def get_blocked_list(self, userProfileId):
with Transaction(self.conn) as tx:
rows = tx.queryall("SELECT * FROM buddies WHERE userProfileId = ? AND blocked = 1", (userProfileId,))
rows = tx.queryall(
"SELECT * FROM buddies"
" WHERE userProfileId = ? AND blocked = 1",
(userProfileId,)
)
users = []
for row in rows:
users.append(self.get_dict(row))
return users
return [self.get_dict(row) for row in rows]
def get_pending_buddy_requests(self, userProfileId):
with Transaction(self.conn) as tx:
rows = tx.queryall("SELECT * FROM buddies WHERE buddyProfileId = ? AND status = 0", (userProfileId,))
rows = tx.queryall(
"SELECT * FROM buddies"
" WHERE buddyProfileId = ? AND status = 0",
(userProfileId,)
)
users = []
for row in rows:
users.append(self.get_dict(row))
return users
return [self.get_dict(row) for row in rows]
def buddy_need_auth_message(self, userProfileId):
with Transaction(self.conn) as tx:
rows = tx.queryall("SELECT * FROM buddies WHERE buddyProfileId = ? AND status = 1 AND notified = 0", (userProfileId,))
rows = tx.queryall(
"SELECT * FROM buddies"
" WHERE buddyProfileId = ? AND status = 1 AND notified = 0",
(userProfileId,)
)
users = []
for row in rows:
users.append(self.get_dict(row))
return users
return [self.get_dict(row) for row in rows]
def buddy_sent_auth_message(self, userProfileId, buddyProfileId):
with Transaction(self.conn) as tx:
tx.nonquery("UPDATE buddies SET notified = ? WHERE userProfileId = ? AND buddyProfileId = ?", (1, userProfileId, buddyProfileId))
tx.nonquery(
"UPDATE buddies SET notified = ?"
" WHERE userProfileId = ? AND buddyProfileId = ?",
(1, userProfileId, buddyProfileId)
)
# Gamestats-related functions
def pd_insert(self, profileid, dindex, ptype, data):
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT COUNT(*) FROM gamestat_profile WHERE profileid = ? AND dindex = ? AND ptype = ?", (profileid, dindex, ptype))
row = tx.queryone(
"SELECT COUNT(*) FROM gamestat_profile"
" WHERE profileid = ? AND dindex = ? AND ptype = ?",
(profileid, dindex, ptype)
)
count = int(row[0])
if count > 0:
tx.nonquery("UPDATE gamestat_profile SET data = ? WHERE profileid = ? AND dindex = ? AND ptype = ?", (data, profileid, dindex, ptype))
tx.nonquery(
"UPDATE gamestat_profile SET data = ?"
" WHERE profileid = ? AND dindex = ? AND ptype = ?",
(data, profileid, dindex, ptype)
)
else:
tx.nonquery("INSERT INTO gamestat_profile (profileid, dindex, ptype, data) VALUES(?,?,?,?)", (profileid, dindex, ptype, data))
tx.nonquery(
"INSERT INTO gamestat_profile"
" (profileid, dindex, ptype, data) VALUES(?,?,?,?)",
(profileid, dindex, ptype, data)
)
def pd_get(self, profileid, dindex, ptype):
with Transaction(self.conn) as tx:
row = tx.queryone("SELECT * FROM gamestat_profile WHERE profileid = ? AND dindex = ? AND ptype = ?", (profileid, dindex, ptype))
row = tx.queryone(
"SELECT * FROM gamestat_profile"
" WHERE profileid = ? AND dindex = ? AND ptype = ?",
(profileid, dindex, ptype)
)
return self.get_dict(row)

View File

@@ -1,31 +1,37 @@
# DWC Network Server Emulator
# Copyright (C) 2014 polaris-
# Copyright (C) 2014 ToadKing
# Copyright (C) 2014 AdmiralCurtiss
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""DWC Network Server Emulator
Copyright (C) 2014 polaris-
Copyright (C) 2014 ToadKing
Copyright (C) 2014 AdmiralCurtiss
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import copy
def parse_gamespy_message(message):
"""Parse a GameSpy message."""
stack = []
messages = {}
msg = message
while len(msg) > 0 and msg[0] == '\\' and "\\final\\" in msg:
# Find the command
# Don't search for more commands if there isn't a \final\, save the left over for the next packet
# Don't search for more commands if there isn't a \final\, save the
# left over for the next packet
found_command = False
while len(msg) > 0 and msg[0] == '\\':
keyEnd = msg[1:].index('\\') + 1
@@ -45,7 +51,7 @@ def parse_gamespy_message(message):
else:
value = msg
if found_command == False:
if not found_command:
messages['__cmd__'] = key
messages['__cmd_val__'] = value
found_command = True
@@ -59,37 +65,39 @@ def parse_gamespy_message(message):
return stack, msg
# Generate a list based on the input dictionary.
# The main command must also be stored in __cmd__ for it to put the parameter at the beginning.
def create_gamespy_message_from_dict(messages_orig):
# Deep copy the dictionary because we don't want the original to be modified
messages = copy.deepcopy(messages_orig)
"""Generate a list based on the input dictionary.
cmd = ""
cmd_val = ""
The main command must also be stored in __cmd__ for it to put the
parameter at the beginning.
"""
# Deep copy the dictionary because we don't want the original to be
# modified
messages = copy.deepcopy(messages_orig)
if "__cmd__" in messages:
cmd = messages['__cmd__']
messages.pop('__cmd__', None)
else:
cmd = ""
if "__cmd_val__" in messages:
cmd_val = messages['__cmd_val__']
messages.pop('__cmd_val__', None)
else:
cmd_val = ""
if cmd in messages:
messages.pop(cmd, None)
l = []
l.append(("__cmd__", cmd))
l.append(("__cmd_val__", cmd_val))
for message in messages:
l.append((message, messages[message]))
l = [("__cmd__", cmd), ("__cmd_val__", cmd_val)]
l.extend([(message, messages[message]) for message in messages])
return l
def create_gamespy_message_from_list(messages):
"""Generate a string based on the input list."""
d = {}
cmd = ""
cmd_val = ""
@@ -101,17 +109,18 @@ def create_gamespy_message_from_list(messages):
elif message[0] == "__cmd_val__":
cmd_val = str(message[1]).strip('\\')
else:
query += "\\%s\\%s" % (str(message[0]).strip('\\'), str(message[1]).strip('\\'))
query += "\\%s\\%s" % (str(message[0]).strip('\\'),
str(message[1]).strip('\\'))
if cmd != "":
if cmd:
# Prepend the main command if one was found.
query = "\\%s\\%s%s" % (cmd, cmd_val, query)
return query
# Create a message based on a dictionary (or list) of parameters.
def create_gamespy_message(messages, id=None):
"""Create a message based on a dictionary (or list) of parameters."""
query = ""
if isinstance(messages, dict):
@@ -119,7 +128,7 @@ def create_gamespy_message(messages, id=None):
# Check for an id if the id needs to be updated.
# If it already exists in the list then update it, else add it
if id != None:
if id is not None:
for message in messages:
if message[0] == "id":
messages.pop(messages.index(message))
@@ -129,7 +138,7 @@ def create_gamespy_message(messages, id=None):
query = create_gamespy_message_from_list(messages)
if id != None:
if id is not None:
query += create_gamespy_message_from_list([("id", id)])
query += "\\final\\"

View File

@@ -1,21 +1,24 @@
# DWC Network Server Emulator
# Copyright (C) 2014 polaris-
# Copyright (C) 2014 ToadKing
# Copyright (C) 2014 AdmiralCurtiss
# Copyright (C) 2014 msoucy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""DWC Network Server Emulator
Copyright (C) 2014 polaris-
Copyright (C) 2014 ToadKing
Copyright (C) 2014 AdmiralCurtiss
Copyright (C) 2014 msoucy
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import base64
import hashlib
@@ -23,11 +26,18 @@ import time
import other.utils as utils
def generate_secret_keys(filename="gslist.cfg"):
"""Generate list of secret keys based on a config file.
gslist.cfg is the default config file and may be incomplete.
TODO: Parse the config file in a cleaner way. (ex: using CSV module)
"""
secret_key_list = {}
with open(filename) as key_file:
for line in key_file.readlines():
#name = line[:54].strip() # Probably won't do anything with the name for now.
# name = line[:54].strip()
# Probably won't do anything with the name for now.
id = line[54:54+19].strip()
key = line[54+19:].strip()
@@ -35,23 +45,37 @@ def generate_secret_keys(filename="gslist.cfg"):
return secret_key_list
# GameSpy uses a slightly modified version of base64 which replaces +/= with []_
def base64_encode(input):
output = base64.b64encode(input).replace('+', '[').replace('/', ']').replace('=', '_')
"""Encode input in base64 using GameSpy variant.
GameSpy uses a slightly modified version of base64 which replaces
+/= with []_
"""
output = base64.b64encode(input).replace('+', '[') \
.replace('/', ']') \
.replace('=', '_')
return output
def base64_decode(input):
output = base64.b64decode(input.replace('[', '+').replace(']', '/').replace('_', '='))
"""Decode input in base64 using GameSpy variant."""
output = base64.b64decode(input.replace('[', '+')
.replace(']', '/')
.replace('_', '='))
return output
# Tetris DS overlay 10 @ 0216E9B8
def rc4_encrypt(_key, _data):
"""
Tetris DS overlay 10 @ 0216E9B8
"""
key = bytearray(_key)
data = bytearray(_data)
if len(key) == 0:
# This shouldn't happen but it apparently can on a rare occasion. key should always be set.
# This shouldn't happen but it apparently can on a rare occasion.
# Key should always be set.
return
# Key-scheduling algorithm
@@ -68,8 +92,9 @@ def rc4_encrypt(_key, _data):
# Pseudo-random generation algorithm + encryption
i = 0
j = 0
for x in range(len(data)):
i = (i + 1 + data[x]) & 0xff # Modified RC4? What's this data[x] doing here?
for x, val in enumerate(data):
# Modified RC4?
i = (i + 1 + val) & 0xff
j = (j + S[i]) & 0xff
S[i], S[j] = S[j], S[i]
@@ -78,41 +103,53 @@ def rc4_encrypt(_key, _data):
return data
# Tetris DS overlay 10 @ 0216E9B8
# Used by the master server to send some data between the client and server.
# This seems to be what Luigi Auriemma calls "Gsmsalg".
def prepare_rc4_base64(_key, _data):
"""Tetris DS overlay 10 @ 0216E9B8
Used by the master server to send some data between the client and server.
This seems to be what Luigi Auriemma calls "Gsmsalg".
"""
data = rc4_encrypt(_key, _data)
if data == None:
if data is None:
data = bytearray()
data.append(0)
return base64.b64encode(buffer(data))
# get the login data from nas.nintendowifi.net/ac from an authtoken
def parse_authtoken(authtoken, db):
"""Get the login data from nas.nintendowifi.net/ac from an authtoken"""
return db.get_nas_login(authtoken)
def login_profile_via_parsed_authtoken(authtoken_parsed, db):
"""Return login profile via parsed authtoken.
authtoken_parsed MUST HAVE userid field and can't be None!
"""
if authtoken_parsed is None or 'userid' not in authtoken_parsed:
return None, None, None, None
console = 0
userid = authtoken_parsed['userid']
csnum = authtoken_parsed.get('csnum', '') # Wii: Serial number
cfc = authtoken_parsed.get('cfc', '') # Wii: Friend code
bssid = authtoken_parsed.get('bssid', '') # NDS: Wifi network's BSSID
devname = authtoken_parsed.get('devname', '') # NDS: Device name
birth = authtoken_parsed.get('birth', '') # NDS: User's birthday
csnum = authtoken_parsed.get('csnum', '') # Wii: Serial number
cfc = authtoken_parsed.get('cfc', '') # Wii: Friend code
bssid = authtoken_parsed.get('bssid', '') # NDS: Wifi network's BSSID
devname = authtoken_parsed.get('devname', '') # NDS: Device name
birth = authtoken_parsed.get('birth', '') # NDS: User's birthday
# The Wii does not use passwd, so take another uniquely generated string as the password.
# The Wii does not use passwd, so take another uniquely generated string
# as the password.
# if "passwd" in authtoken_parsed:
# password = authtoken_parsed['passwd']
# else:
# password = authtoken_parsed['gsbrcd']
# console = 1
if not "passwd" in authtoken_parsed:
if "passwd" not in authtoken_parsed:
console = 1
password = authtoken_parsed['gsbrcd']
@@ -120,7 +157,7 @@ def login_profile_via_parsed_authtoken(authtoken_parsed, db):
gameid = gsbrcd[:4]
macadr = authtoken_parsed['macadr']
uniquenick = utils.base32_encode(int(userid)) + gsbrcd
email = uniquenick + "@nds" # The Wii also seems to use @nds.
email = uniquenick + "@nds" # The Wii also seems to use @nds.
if "csnum" in authtoken_parsed:
console = 1
@@ -128,14 +165,18 @@ def login_profile_via_parsed_authtoken(authtoken_parsed, db):
console = 1
valid_user = db.check_user_exists(userid, gsbrcd)
if valid_user == False:
profileid = db.create_user(userid, password, email, uniquenick, gsbrcd, console, csnum, cfc, bssid, devname, birth, gameid, macadr)
if valid_user is False:
profileid = db.create_user(userid, password, email, uniquenick,
gsbrcd, console, csnum, cfc, bssid,
devname, birth, gameid, macadr)
else:
profileid = db.perform_login(userid, password, gsbrcd)
return userid, profileid, gsbrcd, uniquenick
def generate_response(challenge, ac_challenge, secretkey, authtoken):
"""Generate a challenge response."""
md5 = hashlib.md5()
md5.update(ac_challenge)
@@ -152,9 +193,14 @@ def generate_response(challenge, ac_challenge, secretkey, authtoken):
return md5_2.hexdigest()
# The proof is practically the same thing as the response, except it has the challenge and the secret key swapped.
# Maybe combine the two functions later?
def generate_proof(challenge, ac_challenge, secretkey, authtoken):
"""Generate a challenge proof.
The proof is practically the same thing as the response, except it has
the challenge and the secret key swapped.
Maybe combine the two functions later?
"""
md5 = hashlib.md5()
md5.update(ac_challenge)
@@ -170,8 +216,11 @@ def generate_proof(challenge, ac_challenge, secretkey, authtoken):
return md5_2.hexdigest()
# Code: Tetris DS @ 02057A14
def get_friendcode_from_profileid(profileid, gameid):
"""
Code: Tetris DS @ 02057A14
"""
friendcode = 0
# Combine the profileid and gameid into one buffer
@@ -186,14 +235,20 @@ def get_friendcode_from_profileid(profileid, gameid):
return friendcode
def get_profileid_from_friendcode(friendcode):
"""Return profile ID from Friend Code."""
# Get the lower 32 bits as the profile id
profileid = friendcode & 0xffffffff
return profileid
# Code from Luigi Auriemma's enctypex_decoder.c
# It's kind of sloppy in parts, but it works. Unless there's some issues then it'll probably not change any longer.
class EncTypeX:
"""Code from Luigi Auriemma's enctypex_decoder.c
It's kind of sloppy in parts, but it works. Unless there's some issues
then it'll probably not change any longer.
"""
def __init__(self):
return
@@ -211,7 +266,8 @@ class EncTypeX:
if not key or not validate or not data:
return None
# Convert data from strings to byte arrays before use or else it'll raise an error
# Convert data from strings to byte arrays before use or else
# it'll raise an error
key = bytearray(key)
validate = bytearray(validate)
@@ -233,15 +289,16 @@ class EncTypeX:
data[2] = 0x00
data[header_len - 1] = (tmp_len - header_len) ^ 0xea
header = data[:tmp_len] # The header of the data gets chopped off in init(), so save it
# The header of the data gets chopped off in init(), so save it
header = data[:tmp_len]
encxkey = bytearray([0] * 261)
data = self.init(encxkey, key, validate, data)
self.func6e(encxkey, data, len(data))
# Reappend header that we saved earlier before returning to make the complete buffer
# Reappend header that we saved earlier before returning to make
# the complete buffer
return header + data
def init(self, encxkey, key, validate, data):
data_len = len(data)
@@ -256,9 +313,15 @@ class EncTypeX:
if data_len < (header_len + data_start):
return None
data = self.enctypex_funcx(encxkey, bytearray(key), bytearray(validate), data[header_len:], data_start)
return data[data_start:]
data = self.enctypex_funcx(
encxkey,
bytearray(key),
bytearray(validate),
data[header_len:],
data_start
)
return data[data_start:]
def enctypex_funcx(self, encxkey, key, validate, data, datalen):
keylen = len(key)
@@ -278,7 +341,7 @@ class EncTypeX:
n1 = 0
n2 = 0
for i in range(255,-1,-1):
for i in range(255, -1, -1):
t1, n1, n2 = self.func5(encxkey, i, id, idlen, n1, n2)
t2 = encxkey[i]
encxkey[i] = encxkey[t1]

View File

@@ -1,38 +1,51 @@
# DWC Network Server Emulator
# Copyright (C) 2014 polaris-
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Master server list server
#
# Basic idea:
# The server listing does not need to be persistent, and it must be easily searchable for any unknown parameters.
# So instead of using a SQL database, I've opted to create a server list database server which communicates between the
# server browser and the qr server. The server list database will be stored in dictionaries as to allow dynamic columns
# that can be easily searched. The main reason for this configuration is because it cannot be guaranteed what data
# a game's server will required. For example, in addition to the common fields such as publicip, numplayers, dwc_pid, etc,
# Lost Magic also uses fields such as LMname, LMsecN, LMrating, LMbtmode, and LMversion.
#
# It would be possible to create game-specific databases but this would be more of a hassle and less universal. It would
# also be possible pickle a dictionary containing all of the fields and store it in a SQL database instead, but that
# would require unpickling every server each time you want to match search queries which would cause overhead if there
# are a lot of running servers. One trade off here is that we'll be using more memory by storing each server as a
# dictionary in the memory instead of storing it in a SQL database.
#
# qr_server and server_browser both will act as clients to gs_server_database.
# qr_server will send an add and/or delete to add or remove servers from the server list.
# server_browser will send a request with the game name followed by optional search parameters to get a list of servers.
"""DWC Network Server Emulator
Copyright (C) 2014 polaris-
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Master server list server
Basic idea:
The server listing does not need to be persistent, and it must be easily
searchable for any unknown parameters. So instead of using a SQL database,
I've opted to create a server list database server which communicates
between the server browser and the qr server. The server list database
will be stored in dictionaries as to allow dynamic columns that can be
easily searched. The main reason for this configuration is because it
cannot be guaranteed what data a game's server will required. For example,
in addition to the common fields such as publicip, numplayers, dwc_pid,
etc, Lost Magic also uses fields such as LMname, LMsecN, LMrating,
LMbtmode, and LMversion.
It would be possible to create game-specific databases but this would be
more of a hassle and less universal. It would also be possible pickle a
dictionary containing all of the fields and store it in a SQL database
instead, but that would require unpickling every server each time you want
to match search queries which would cause overhead if there are a lot of
running servers. One trade off here is that we'll be using more memory by
storing each server as a dictionary in the memory instead of storing it in
a SQL database.
- qr_server and server_browser both will act as clients to
gs_server_database.
- qr_server will send an add and/or delete to add or remove servers from the
server list.
- server_browser will send a request with the game name followed by optional
search parameters to get a list of servers.
"""
import logging
import time
@@ -41,6 +54,10 @@ import ast
from multiprocessing.managers import BaseManager
from multiprocessing import freeze_support
import other.utils as utils
import dwc_config
logger = dwc_config.get_logger('GameSpyManager')
class TokenType:
UNKNOWN = 0
@@ -49,50 +66,83 @@ class TokenType:
NUMBER = 3
TOKEN = 4
# Logger settings
logger_output_to_console = True
logger_output_to_file = True
logger_name = "GamespyBackendServer"
logger_filename = "gamespy_backend_server.log"
logger = utils.create_logger(logger_name, logger_filename, -1, logger_output_to_console, logger_output_to_file)
class GameSpyServerDatabase(BaseManager):
pass
class GameSpyBackendServer(object):
def __init__(self):
self.server_list = {}
self.natneg_list = {}
GameSpyServerDatabase.register("get_server_list", callable=lambda:self.server_list)
GameSpyServerDatabase.register("find_servers", callable=self.find_servers)
GameSpyServerDatabase.register("find_server_by_address", callable=self.find_server_by_address)
GameSpyServerDatabase.register("find_server_by_local_address", callable=self.find_server_by_local_address)
GameSpyServerDatabase.register("update_server_list", callable=self.update_server_list)
GameSpyServerDatabase.register("delete_server", callable=self.delete_server)
GameSpyServerDatabase.register("add_natneg_server", callable=self.add_natneg_server)
GameSpyServerDatabase.register("get_natneg_server", callable=self.get_natneg_server)
GameSpyServerDatabase.register("delete_natneg_server", callable=self.delete_natneg_server)
GameSpyServerDatabase.register(
"get_server_list",
callable=lambda: self.server_list
)
GameSpyServerDatabase.register(
"find_servers",
callable=self.find_servers
)
GameSpyServerDatabase.register(
"find_server_by_address",
callable=self.find_server_by_address
)
GameSpyServerDatabase.register(
"find_server_by_local_address",
callable=self.find_server_by_local_address
)
GameSpyServerDatabase.register(
"update_server_list",
callable=self.update_server_list
)
GameSpyServerDatabase.register(
"delete_server",
callable=self.delete_server
)
GameSpyServerDatabase.register(
"add_natneg_server",
callable=self.add_natneg_server
)
GameSpyServerDatabase.register(
"get_natneg_server",
callable=self.get_natneg_server
)
GameSpyServerDatabase.register(
"delete_natneg_server",
callable=self.delete_natneg_server
)
def start(self):
address = ("127.0.0.1", 27500)
address = dwc_config.get_ip_port('GameSpyManager')
password = ""
logger.log(logging.INFO, "Started server on %s:%d..." % (address[0], address[1]))
logger.log(logging.INFO,
"Started server on %s:%d...",
address[0], address[1])
manager = GameSpyServerDatabase(address = address, authkey = password)
manager = GameSpyServerDatabase(address=address,
authkey=password)
server = manager.get_server()
server.serve_forever()
def get_token(self, filters):
# Complex example from Dungeon Explorer: Warriors of Ancient Arts
# dwc_mver = 3 and dwc_pid != 474890913 and maxplayers = 2 and numplayers < 2 and dwc_mtype = 0 and dwc_mresv != dwc_pid and (MatchType='english')
#
# Even more complex example from Phantasy Star Zero:
# dwc_mver = 3 and dwc_pid != 4 and maxplayers = 3 and numplayers < 3 and dwc_mtype = 0 and dwc_mresv != dwc_pid and (((20=auth)AND((1&mskdif)=mskdif)AND((14&mskstg)=mskstg)))
#
# Example with OR from Mario Kart Wii:
# dwc_mver = 90 and dwc_pid != 1 and maxplayers = 11 and numplayers < 11 and dwc_mtype = 0 and dwc_hoststate = 2 and dwc_suspend = 0 and (rk = 'vs_123' and (ev > 4263 or ev <= 5763) and p = 0)
"""Complex example from Dungeon Explorer: Warriors of Ancient Arts
dwc_mver = 3 and dwc_pid != 474890913 and maxplayers = 2 and
numplayers < 2 and dwc_mtype = 0 and dwc_mresv != dwc_pid and
(MatchType='english')
Even more complex example from Phantasy Star Zero:
dwc_mver = 3 and dwc_pid != 4 and maxplayers = 3 and
numplayers < 3 and dwc_mtype = 0 and dwc_mresv != dwc_pid and
(((20=auth)AND((1&mskdif)=mskdif)AND((14&mskstg)=mskstg)))
Example with OR from Mario Kart Wii:
dwc_mver = 90 and dwc_pid != 1 and maxplayers = 11 and
numplayers < 11 and dwc_mtype = 0 and dwc_hoststate = 2 and
dwc_suspend = 0 and (rk = 'vs_123' and (ev > 4263 or ev <= 5763)
and p = 0)
"""
i = 0
start = i
special_chars = "_"
@@ -129,7 +179,8 @@ class GameSpyBackendServer(object):
# >= or <=
i += 1
elif i + 1 < len(filters) and filters[i] == "!" and filters[i + 1] == "=":
elif i + 1 < len(filters) and filters[i] == "!" and \
filters[i + 1] == "=":
i += 2
token_type = TokenType.TOKEN
@@ -137,25 +188,27 @@ class GameSpyBackendServer(object):
# String literal
token_type = TokenType.STRING
i += 1 # Skip quotation mark
i += 1 # Skip quotation mark
while i < len(filters) and filters[i] != "'":
i += 1
if i < len(filters) and filters[i] == "'":
i += 1 # Skip quotation mark
i += 1 # Skip quotation mark
elif filters[i] == "\"":
# I don't know if it's in the spec or not, but I added "" string literals as well just in case.
# I don't know if it's in the spec or not, but I added ""
# string literals as well just in case.
token_type = TokenType.STRING
i += 1 # Skip quotation mark
i += 1 # Skip quotation mark
while i < len(filters) and filters[i] != "\"":
i += 1
if i < len(filters) and filters[i] == "\"":
i += 1 # Skip quotation mark
i += 1 # Skip quotation mark
elif i + 1 < len(filters) and filters[i] == '-' and filters[i + 1].isdigit():
elif i + 1 < len(filters) and filters[i] == '-' and \
filters[i + 1].isdigit():
# Negative number
token_type = TokenType.NUMBER
i += 1
@@ -169,11 +222,14 @@ class GameSpyBackendServer(object):
elif filters[i].isalpha():
token_type = TokenType.FIELD
while i < len(filters) and (filters[i].isalnum() or filters[i] in special_chars) and filters[i] not in "!=>< ":
while i < len(filters) and (filters[i].isalnum() or
filters[i] in special_chars) and \
filters[i] not in "!=>< ":
i += 1
token = filters[start:i]
if token_type == TokenType.FIELD and (token.lower() == "and" or token.lower() == "or"):
if token_type == TokenType.FIELD and \
(token.lower() == "and" or token.lower() == "or"):
token = token.lower()
return token, i, token_type
@@ -187,7 +243,8 @@ class GameSpyBackendServer(object):
filters = filters[i:]
if token_type == TokenType.TOKEN:
# Python uses == instead of = for comparisons, so replace it with the proper token for compilation.
# Python uses == instead of = for comparisons, so replace
# it with the proper token for compilation.
if token == "=":
token = "=="
@@ -200,16 +257,20 @@ class GameSpyBackendServer(object):
return output, variables
def validate_ast(self, node, num_literal_only):
# This function tries to verify that the expression is a valid expression before it gets evaluated.
# Anything besides the whitelisted things below are strictly forbidden:
# This function tries to verify that the expression is a valid
# expression before it gets evaluated.
# Anything besides the whitelisted things below are strictly
# forbidden:
# - String literals
# - Number literals
# - Binary operators (CAN ONLY BE PERFORMED ON TWO NUMBER LITERALS)
# - Comparisons (cannot use 'in', 'not in', 'is', 'is not' operators)
#
# Anything such as variables or arrays or function calls are NOT VALID.
# Never run the expression received from the client before running this function on the expression first.
#print type(node)
# Anything such as variables or arrays or function calls are NOT
# VALID.
# Never run the expression received from the client before running
# this function on the expression first.
# print type(node)
# Only allow literals, comparisons, and math operations
valid_node = False
@@ -217,20 +278,20 @@ class GameSpyBackendServer(object):
valid_node = True
elif isinstance(node, ast.Str):
if num_literal_only == False:
if not num_literal_only:
valid_node = True
elif isinstance(node, ast.BoolOp):
for value in node.values:
valid_node = self.validate_ast(value, num_literal_only)
if valid_node == False:
if not valid_node:
break
elif isinstance(node, ast.BinOp):
valid_node = self.validate_ast(node.left, True)
if valid_node == True:
if valid_node:
valid_node = self.validate_ast(node.right, True)
elif isinstance(node, ast.UnaryOp):
@@ -243,16 +304,18 @@ class GameSpyBackendServer(object):
valid_node = self.validate_ast(node.left, num_literal_only)
for op in node.ops:
#print type(op)
# print type(op)
# Restrict "is", "is not", "in", and "not in" python comparison operators.
# These are python-specific and the games have no way of knowing what they are, so there's no reason
# to keep them around.
if isinstance(op, ast.Is) or isinstance(op, ast.IsNot) or isinstance(op, ast.In) or isinstance(op, ast.NotIn):
# Restrict "is", "is not", "in", and "not in" python
# comparison operators. These are python-specific and the
# games have no way of knowing what they are, so there's no
# reason to keep them around.
if isinstance(op, ast.Is) or isinstance(op, ast.IsNot) or \
isinstance(op, ast.In) or isinstance(op, ast.NotIn):
valid_node = False
break
if valid_node == True:
if valid_node:
for expr in node.comparators:
valid_node = self.validate_ast(expr, num_literal_only)
@@ -261,7 +324,6 @@ class GameSpyBackendServer(object):
return valid_node
def find_servers(self, gameid, filters, fields, max_count):
matched_servers = []
@@ -273,7 +335,7 @@ class GameSpyBackendServer(object):
for server in self.server_list[gameid]:
stop_search = False
if filters != "":
if filters:
translated, variables = self.translate_expression(filters)
for idx in variables:
@@ -286,69 +348,83 @@ class GameSpyBackendServer(object):
if token_type == TokenType.FIELD:
# At this point, any field should be a string.
# This does not support stuff like:
# dwc_test = 'test', dwc_test2 = dwc_test, dwc_test3 = dwc_test2
# dwc_test = 'test', dwc_test2 = dwc_test,
# dwc_test3 = dwc_test2
token = '"' + token + '"'
elif token_type == TokenType.NUMBER:
for idx2 in range(idx + 1, len(translated)):
_, _, token_type = self.get_token(translated[idx2])
_, _, token_type = \
self.get_token(translated[idx2])
if token_type == TokenType.TOKEN and translated[idx2] not in ('(', ')'):
if token_type == TokenType.TOKEN and \
translated[idx2] not in ('(', ')'):
if idx2 == idx + 1:
# Skip boolean operator if it's the first token on the right
# Skip boolean operator if it's the
# first token on the right
continue
# Boolean operator, leave left as integer
token = str(int(token))
break
elif token_type == TokenType.STRING or token_type == TokenType.NUMBER:
elif token_type == TokenType.STRING or \
token_type == TokenType.NUMBER:
if token_type == TokenType.STRING:
# Found string on far right, turn left into string as well
# Found string on far right, turn left
# into string as well
token = "'" + token + "'"
elif token_type == TokenType.NUMBER:
token = str(int(token))
break
translated[idx] = token
q = ' '.join(translated)
# Always run validate_ast over the entire AST before evaluating anything. eval() is dangerous to use on
# unsanitized inputs. The validate_ast function has a fairly strict whitelist so it should be safe in what
# it accepts as valid.
# Always run validate_ast over the entire AST before
# evaluating anything. eval() is dangerous to use on
# unsanitized inputs. The validate_ast function has a fairly
# strict whitelist so it should be safe in what it accepts as
# valid.
m = ast.parse(q, "<string>", "exec")
valid_filter = True
for node in m.body:
valid_filter = self.validate_ast(node, False)
if valid_filter == False:
if not valid_filter:
# Return only anything matched up until this point.
logger.log(logging.WARNING, "Invalid filter(s): %s" % (filters))
#stop_search = True
logger.log(logging.WARNING,
"Invalid filter(s): %s",
filters)
# stop_search = True
continue
else:
# Use Python to evaluate the query. This method may take a little time but it shouldn't be all that
# big of a difference, I think. It takes about 0.0004 seconds per server to determine whether or not it's a
# match on my computer. Usually there's a low max_servers set when the game searches for servers, so assuming
# something like the game is asking for 6 servers, it would take about 0.0024 seconds total. These times
# will obviously be different per computer. It's not ideal, but it shouldn't be a huge bottleneck.
# A possible way to speed it up is to make validate_ast also evaluate the expressions at the same time as it
# validates it.
# Use Python to evaluate the query. This method may take a
# little time but it shouldn't be all that big of a
# difference, I think. It takes about 0.0004 seconds per
# server to determine whether or not it's a match on my
# computer. Usually there's a low max_servers set when the
# game searches for servers, so assuming something like
# the game is asking for 6 servers, it would take about
# 0.0024 seconds total. These times will obviously be
# different per computer. It's not ideal, but it shouldn't
# be a huge bottleneck. A possible way to speed it up is
# to make validate_ast also evaluate the expressions at
# the same time as it validates it.
result = eval(q)
else:
# There are no filters, so just return the server.
result = True
valid_filter = True
if stop_search == True:
if stop_search:
break
if valid_filter == True and result == True:
if valid_filter and result:
matched_servers.append(server)
if max_count != 0 and len(matched_servers) >= max_count:
if max_count and len(matched_servers) >= max_count:
break
servers = []
@@ -367,70 +443,83 @@ class GameSpyBackendServer(object):
"publicip", "publicport",
"__session__", "__console__"
]
result.update({name:server[name] for name in attrs if name in server})
result.update({name: server[name]
for name in attrs if name in server})
requested = {}
for field in fields:
#if not field in result:
# if not field in result:
if field in server:
requested[field] = server[field]
else:
# Return a dummy value. What's the normal behavior of the real server in this case?
# Return a dummy value. What's the normal behavior of
# the real server in this case?
requested[field] = ""
result['requested'] = requested
servers.append(result)
logger.log(logging.DEBUG, "Matched %d servers in %s seconds" % (len(servers), (time.time() - start)))
logger.log(logging.DEBUG,
"Matched %d servers in %s seconds",
len(servers), (time.time() - start))
return servers
def update_server_list(self, gameid, session, value, console):
# Make sure the user isn't hosting multiple servers or there isn't some left over server information that
# never got handled properly (game crashed, etc).
"""Make sure the user isn't hosting multiple servers or there isn't
some left over server information that never got handled properly
(game crashed, etc)."""
self.delete_server(gameid, session)
# If the game doesn't exist already, create a new list.
if not gameid in self.server_list:
if gameid not in self.server_list:
self.server_list[gameid] = []
# Add new server
value['__session__'] = session
value['__console__'] = console
logger.log(logging.DEBUG, "Added %s to the server list for %s" % (value, gameid))
logger.log(logging.DEBUG,
"Added %s to the server list for %s",
value, gameid)
self.server_list[gameid].append(value)
logger.log(logging.DEBUG, "%s servers: %d" % (gameid, len(self.server_list[gameid])))
logger.log(logging.DEBUG,
"%s servers: %d",
gameid, len(self.server_list[gameid]))
return value
def delete_server(self, gameid, session):
if not gameid in self.server_list:
if gameid not in self.server_list:
# Nothing to do if no servers for that game even exist.
return
# Remove all servers hosted by the given session id.
count = len(self.server_list[gameid])
self.server_list[gameid] = [x for x in self.server_list[gameid] if x['__session__'] != session]
self.server_list[gameid] = [x for x in self.server_list[gameid]
if x['__session__'] != session]
count -= len(self.server_list[gameid])
logger.log(logging.DEBUG, "Deleted %d %s servers where session = %d" % (count, gameid, session))
logger.log(logging.DEBUG,
"Deleted %d %s servers where session = %d",
count, gameid, session)
def find_server_by_address(self, ip, port, gameid = None):
if gameid == None:
def find_server_by_address(self, ip, port, gameid=None):
if gameid is None:
# Search all servers
for gameid in self.server_list:
for server in self.server_list[gameid]:
if server['publicip'] == ip and (port == 0 or server['publicport'] == str(port)):
if server['publicip'] == ip and \
(not port or server['publicport'] == str(port)):
return server
else:
for server in self.server_list[gameid]:
if server['publicip'] == ip and (port == 0 or server['publicport'] == str(port)):
if server['publicip'] == ip and \
(not port or server['publicport'] == str(port)):
return server
return None
def find_server_by_local_address(self, publicip, localaddr, gameid = None):
def find_server_by_local_address(self, publicip, localaddr, gameid=None):
localip = localaddr[0]
localport = localaddr[1]
localip_int_le = localaddr[2]
@@ -440,31 +529,41 @@ class GameSpyBackendServer(object):
best_match = None
for server in self.server_list[gameid]:
logger.log(logging.DEBUG, "publicip: %s == %s ? %d localport: %s == %s ? %d" % (server['publicip'], publicip, server['publicip'] == publicip, server['localport'], str(localport), server['localport'] == str(localport)))
logger.log(logging.DEBUG,
"publicip: %s == %s ? %d localport: %s == %s ? %d",
server['publicip'], publicip,
server['publicip'] == publicip,
server['localport'],
str(localport),
server['localport'] == str(localport))
if server['publicip'] == publicip:
if server['localport'] == str(localport):
best_match = server
break
for x in range(0, 10):
s = 'localip%d' % x
if s in server:
if server[s] == localip:
best_match = server
s = 'localip%d' % x
if s in server:
if server[s] == localip:
best_match = server
if localport == 0 and best_match == None:
if not localport and best_match is None:
# Kinda hackish. This sometimes happens.
# Assuming two clients aren't trying to connect from the same IP, this might be safe.
# The server wasn't verified to be the *correct* server, but it's on the same IP so it
# has a chance of being correct. At least make an attempt to establish the connection.
# Assuming two clients aren't trying to connect from
# the same IP, this might be safe. The server wasn't
# verified to be the *correct* server, but it's on the
# same IP so it has a chance of being correct. At
# least make an attempt to establish the connection.
best_match = server
if best_match == None:
logger.log(logging.DEBUG, "Couldn't find a match for %s" % (publicip))
if best_match is None:
logger.log(logging.DEBUG,
"Couldn't find a match for %s",
publicip)
return best_match
if gameid == None:
if gameid is None:
# Search all servers
for gameid in self.server_list:
return find_server(gameid)
@@ -477,7 +576,7 @@ class GameSpyBackendServer(object):
if cookie not in self.natneg_list:
self.natneg_list[cookie] = []
logger.log(logging.DEBUG, "Added natneg server %d" % (cookie))
logger.log(logging.DEBUG, "Added natneg server %d", cookie)
self.natneg_list[cookie].append(server)
def get_natneg_server(self, cookie):
@@ -487,10 +586,10 @@ class GameSpyBackendServer(object):
return None
def delete_natneg_server(self, cookie):
# TODO: Find a good time to prune the natneg server listing.
"""TODO: Find a good time to prune the natneg server listing."""
if cookie in self.natneg_list:
del self.natneg_list[cookie]
logger.log(logging.DEBUG, "Deleted natneg server %d" % (cookie))
logger.log(logging.DEBUG, "Deleted natneg server %d", cookie)
if __name__ == '__main__':

View File

@@ -1,21 +1,24 @@
# DWC Network Server Emulator
# Copyright (C) 2014 polaris-
# Copyright (C) 2014 ToadKing
# Copyright (C) 2014 AdmiralCurtiss
# Copyright (C) 2014 msoucy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""DWC Network Server Emulator
Copyright (C) 2014 polaris-
Copyright (C) 2014 ToadKing
Copyright (C) 2014 AdmiralCurtiss
Copyright (C) 2014 msoucy
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging
import time
@@ -32,26 +35,25 @@ import gamespy.gs_database as gs_database
import gamespy.gs_query as gs_query
import gamespy.gs_utility as gs_utils
import other.utils as utils
import dwc_config
# Logger settings
logger_output_to_console = True
logger_output_to_file = True
logger_name = "GameSpyGamestatsServer"
logger_filename = "gamespy_gamestats_server.log"
logger = utils.create_logger(logger_name, logger_filename, -1, logger_output_to_console, logger_output_to_file)
logger = dwc_config.get_logger('GameSpyGamestatsServer')
address = dwc_config.get_ip_port('GameSpyGamestatsServer')
address = ("0.0.0.0", 29920)
class GameSpyGamestatsServer(object):
def __init__(self):
pass
def start(self):
endpoint_search = serverFromString(reactor, "tcp:%d:interface=%s" % (address[1], address[0]))
endpoint_search = serverFromString(
reactor,
"tcp:%d:interface=%s" % (address[1], address[0])
)
conn_search = endpoint_search.listen(GamestatsFactory())
try:
if reactor.running == False:
if not reactor.running:
reactor.run(installSignalHandlers=0)
except ReactorAlreadyRunning:
pass
@@ -59,7 +61,9 @@ class GameSpyGamestatsServer(object):
class GamestatsFactory(Factory):
def __init__(self):
logger.log(logging.INFO, "Now listening for connections on %s:%d...", address[0], address[1])
logger.log(logging.INFO,
"Now listening for connections on %s:%d...",
address[0], address[1])
self.sessions = {}
def buildProtocol(self, address):
@@ -68,13 +72,15 @@ class GamestatsFactory(Factory):
class Gamestats(LineReceiver):
def __init__(self, sessions, address):
self.setRawMode() # We're dealing with binary data so set to raw mode
self.setRawMode() # We're dealing with binary data so set to raw mode
self.db = gs_database.GamespyDatabase()
self.sessions = sessions
self.address = address
self.remaining_message = "" # Stores any unparsable/incomplete commands until the next rawDataReceived
# Stores any unparsable/incomplete commands until the next
# rawDataReceived
self.remaining_message = ""
self.session = ""
self.gameid = ""
@@ -83,26 +89,39 @@ class Gamestats(LineReceiver):
self.data = ""
def log(self, level, message):
if self.session == "":
if self.gameid == "":
logger.log(level, "[%s:%d] %s", self.address.host, self.address.port,message)
def log(self, level, msg, *args, **kwargs):
if not self.session:
if not self.gameid:
logger.log(level, "[%s:%d] " + msg,
self.address.host, self.address.port,
*args, **kwargs)
else:
logger.log(level, "[%s:%d | %s] %s", self.address.host, self.address.port, self.gameid, message)
logger.log(level, "[%s:%d | %s] " + msg,
self.address.host, self.address.port, self.gameid,
*args, **kwargs)
else:
if self.gameid == "":
logger.log(level, "[%s:%d | %s] %s", self.address.host, self.address.port, self.session, message)
if not self.gameid:
logger.log(level, "[%s:%d | %s] " + msg,
self.address.host, self.address.port, self.session,
*args, **kwargs)
else:
logger.log(level, "[%s:%d | %s | %s] %s", self.address.host, self.address.port, self.session, self.gameid, message)
logger.log(level, "[%s:%d | %s | %s] " + msg,
self.address.host, self.address.port, self.session,
self.gameid, *args, **kwargs)
def connectionMade(self):
try:
self.log(logging.INFO, "Received connection from %s:%d" % (self.address.host, self.address.port))
self.log(logging.INFO,
"Received connection from %s:%d",
self.address.host, self.address.port)
# Generate a random challenge string
self.challenge = utils.generate_random_str(10, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
self.challenge = utils.generate_random_str(
10, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
# The first command sent to the client is always a login challenge containing the server challenge key.
# The first command sent to the client is always a login challenge
# containing the server challenge key.
msg = gs_query.create_gamespy_message([
('__cmd__', "lc"),
('__cmd_val__', "1"),
@@ -110,12 +129,14 @@ class Gamestats(LineReceiver):
('id', "1"),
])
self.log(logging.DEBUG, "SENDING: '%s'..." % msg)
self.log(logging.DEBUG, "SENDING: '%s'...", msg)
msg = self.crypt(msg)
self.transport.write(bytes(msg))
except:
self.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
self.log(logging.ERROR,
"Unknown exception: %s",
traceback.format_exc())
def connectionLost(self, reason):
return
@@ -132,31 +153,36 @@ class Gamestats(LineReceiver):
self.data = msg
self.remaining_message = ""
commands, self.remaining_message = gs_query.parse_gamespy_message(msg)
logger.log(logging.DEBUG, "STATS RESPONSE: %s" % msg)
commands, self.remaining_message = \
gs_query.parse_gamespy_message(msg)
logger.log(logging.DEBUG, "STATS RESPONSE: %s", msg)
cmds = {
"auth": self.perform_auth,
"authp": self.perform_authp,
"ka": self.perform_ka,
"setpd": self.perform_setpd,
"getpd": self.perform_getpd,
"newgame": self.perform_newgame,
"updgame": self.perform_updgame,
"auth": self.perform_auth,
"authp": self.perform_authp,
"ka": self.perform_ka,
"setpd": self.perform_setpd,
"getpd": self.perform_getpd,
"newgame": self.perform_newgame,
"updgame": self.perform_updgame,
}
def cmd_err(data_parsed):
logger.log(logging.DEBUG, "Found unknown command, don't know how to handle '%s'.", data_parsed['__cmd__'])
logger.log(logging.DEBUG,
"Found unknown command, don't know how to"
" handle '%s'.", data_parsed['__cmd__'])
for data_parsed in commands:
print(data_parsed)
cmds.get(data_parsed['__cmd__'], cmd_err)(data_parsed)
except:
self.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
self.log(logging.ERROR,
"Unknown exception: %s",
traceback.format_exc())
def perform_auth(self, data_parsed):
self.log(logging.DEBUG, "Parsing 'auth'...")
self.log(logging.DEBUG, "%s", "Parsing 'auth'...")
if "gamename" in data_parsed:
self.gameid = data_parsed['gamename']
@@ -171,22 +197,26 @@ class Gamestats(LineReceiver):
('id', "1"),
])
self.log(logging.DEBUG, "SENDING: '%s'..." % msg)
self.log(logging.DEBUG, "SENDING: '%s'...", msg)
msg = self.crypt(msg)
self.transport.write(bytes(msg))
def perform_authp(self, data_parsed):
authtoken_parsed = gs_utils.parse_authtoken(data_parsed['authtoken'], self.db)
#print authtoken_parsed
authtoken_parsed = gs_utils.parse_authtoken(data_parsed['authtoken'],
self.db)
# print authtoken_parsed
if "lid" in data_parsed:
self.lid = data_parsed['lid']
userid, profileid, gsbrcd, uniquenick = gs_utils.login_profile_via_parsed_authtoken(authtoken_parsed, self.db)
userid, profileid, gsbrcd, uniquenick = \
gs_utils.login_profile_via_parsed_authtoken(authtoken_parsed,
self.db)
if profileid != None:
# Successfully logged in or created account, continue creating session.
if profileid is not None:
# Successfully logged in or created account, continue
# creating session.
sesskey = self.db.create_session(profileid, '')
self.sessions[profileid] = self
self.profileid = int(profileid)
@@ -206,7 +236,7 @@ class Gamestats(LineReceiver):
('errmsg', 'Invalid Validation'),
])
self.log(logging.DEBUG, "SENDING: '%s'..." % msg)
self.log(logging.DEBUG, "SENDING: '%s'...", msg)
msg = self.crypt(msg)
self.transport.write(bytes(msg))
@@ -217,7 +247,7 @@ class Gamestats(LineReceiver):
('__cmd_val__', ""),
])
self.log(logging.DEBUG, "SENDING: '%s'..." % msg)
self.log(logging.DEBUG, "SENDING: '%s'...", msg)
msg = self.crypt(msg)
self.transport.write(bytes(msg))
@@ -234,80 +264,106 @@ class Gamestats(LineReceiver):
('mod', int(time.time())),
])
self.log(logging.DEBUG, "SENDING: '%s'..." % msg)
self.log(logging.DEBUG, "SENDING: '%s'...", msg)
msg = self.crypt(msg)
self.transport.write(bytes(msg))
# TODO: Return error message.
if int(data_parsed['pid']) != self.profileid:
logger.log(logging.WARNING, "ERROR: %d tried to update %d's profile" % (int(data_parsed['pid']), self.profileid))
logger.log(logging.WARNING,
"ERROR: %d tried to update %d's profile",
int(data_parsed['pid']), self.profileid)
return
data_str = "\\data\\"
length = int(data_parsed['length'])
if len(data) < length:
# The packet isn't complete yet, keep loop until we get the entire packet.
# The length entire packet SHOULD always be greater than the data field, so this check should be fine.
# The packet isn't complete yet, keep loop until we get the
# entire packet. The length entire packet SHOULD always be
# greater than the data field, so this check should be fine.
return
if data_str in data:
idx = data.index(data_str) + len(data_str)
data = data[idx:idx+length].rstrip("\\")
data = data[idx:idx + length].rstrip("\\")
else:
logger.log(logging.ERROR, "ERROR: Could not find \data\ in setpd command: %s", data)
logger.log(logging.ERROR,
"ERROR: Could not find \data\ in setpd command: %s",
data)
data = ""
current_data = self.db.pd_get(self.profileid, data_parsed['dindex'], data_parsed['ptype'])
current_data = self.db.pd_get(self.profileid,
data_parsed['dindex'],
data_parsed['ptype'])
if current_data and data and 'data' in current_data:
current_data = current_data['data'].lstrip('\\').split('\\')
new_data = data.lstrip('\\').split('\\')
current_data = dict(zip(current_data[0::2],current_data[1::2]))
new_data = dict(zip(new_data[0::2],new_data[1::2]))
current_data = dict(zip(current_data[0::2],
current_data[1::2]))
new_data = dict(zip(new_data[0::2], new_data[1::2]))
for k in new_data.keys():
current_data[k] = new_data[k]
# TODO: use str.join()
data = "\\"
for k in current_data.keys():
data += k+"\\"+current_data[k]+"\\"
data = data.rstrip("\\") # Don't put trailing \ into db
data += k + "\\" + current_data[k] + "\\"
data = data.rstrip("\\") # Don't put trailing \ into db
self.db.pd_insert(self.profileid, data_parsed['dindex'], data_parsed['ptype'], data)
self.db.pd_insert(self.profileid,
data_parsed['dindex'],
data_parsed['ptype'],
data)
def perform_getpd(self, data_parsed):
pid = int(data_parsed['pid'])
profile = self.db.pd_get(pid, data_parsed['dindex'], data_parsed['ptype'])
profile = self.db.pd_get(pid,
data_parsed['dindex'],
data_parsed['ptype'])
if profile == None:
self.log(logging.WARNING, "Could not find profile for %d %s %s" % (pid, data_parsed['dindex'], data_parsed['ptype']))
if profile is None:
self.log(logging.WARNING,
"Could not find profile for %d %s %s",
pid, data_parsed['dindex'], data_parsed['ptype'])
keys = data_parsed['keys'].split('\x01')
profile_data = None
data = ""
# Someone figure out if this is actually a good way to handle this when no profile is found
if profile != None and 'data' in profile:
# Someone figure out if this is actually a good way to handle this
# when no profile is found
if profile is not None and 'data' in profile:
profile_data = profile['data']
if profile_data.endswith("\\"):
profile_data = profile_data[:-1]
profile_data = gs_query.parse_gamespy_message("\\prof\\" + profile_data + "\\final\\")
profile_data = \
gs_query.parse_gamespy_message("\\prof\\" + profile_data +
"\\final\\")
if profile_data != None:
if profile_data is not None:
profile_data = profile_data[0][0]
else:
self.log(logging.WARNING, "Could not get data section from profile for %d" % pid)
self.log(logging.WARNING,
"Could not get data section from profile for %d",
pid)
if len(keys):
for key in (key for key in keys if key not in ("__cmd__", "__cmd_val__", "")):
# TODO: more clean/pythonic way to do (join?)
for key in keys:
if key in ("__cmd__", "__cmd_val__", ""):
continue
data += "\\" + key + "\\"
if profile_data != None and key in profile_data:
if profile_data is not None and key in profile_data:
data += profile_data[key]
else:
self.log(logging.WARNING, "No keys requested, defaulting to all keys: %s" % (profile['data']))
self.log(logging.WARNING,
"No keys requested, defaulting to all keys: %s",
profile['data'])
data = profile['data']
modified = int(time.time())
@@ -322,30 +378,31 @@ class Gamestats(LineReceiver):
('data', data),
])
msg = msg.replace("\\data\\","\\data\\\\") # data needs to be preceded by an extra slash
# data needs to be preceded by an extra slash
msg = msg.replace("\\data\\", "\\data\\\\")
datastring = ""
try:
datastring = re.findall('.*data\\\\(.*)',msg)[0].replace("\\final\\","")
datastring = re.findall('.*data\\\\(.*)', msg)[0] \
.replace("\\final\\", "")
except:
pass
# This works because the data string is a key-value pair, splitting the
# string by \ should yield a list with an even number of elements. But,
# because of the extra \ prepended to the datastring, it'll be odd.
# So ultimately I expect the list to have an odd number of elements.
# If it's even, len(list)%2 will be zero... and that means the last
# field in the datastring is empty and doesn't have a closing \.
# This works because the data string is a key-value pair, splitting
# the string by \ should yield a list with an even number of elements.
# But, because of the extra \ prepended to the datastring, it'll be
# odd. So ultimately I expect the list to have an odd number of
# elements. If it's even, len(list)%2 will be zero... and that means
# the last field in the datastring is empty and doesn't have a
# closing \.
if datastring and not len(datastring.split('\\')) % 2:
msg = msg.replace("\\final\\","\\\\final\\") # An empty field must be terminated by \ before \final\
# An empty field must be terminated by \ before \final\
msg = msg.replace("\\final\\", "\\\\final\\")
self.log(logging.DEBUG, "SENDING: '%s'..." % msg)
self.log(logging.DEBUG, "SENDING: '%s'...", msg)
msg = self.crypt(msg)
self.transport.write(bytes(msg))
def perform_newgame(self, data_parsed):
# No op
return
@@ -355,7 +412,8 @@ class Gamestats(LineReceiver):
return
def crypt(self, data):
key = "GameSpy3D"
key = bytearray(b"GameSpy3D")
key_len = len(key)
output = bytearray(data.encode("ascii"))
if "\\final\\" in output:
@@ -364,11 +422,11 @@ class Gamestats(LineReceiver):
end = len(output)
for i in range(end):
output[i] ^= ord(key[i % len(key)])
output[i] ^= key[i % key_len]
return output
if __name__ == "__main__":
gsss = GameSpyGamestatsServer()
gsss.start()

View File

@@ -1,22 +1,27 @@
# DWC Network Server Emulator
# Copyright (C) 2014 polaris-
# Copyright (C) 2014 ToadKing
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Server emulator for *.available.gs.nintendowifi.net and *.master.gs.nintendowifi.net
# Query and Reporting: http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Overview
"""DWC Network Server Emulator
Copyright (C) 2014 polaris-
Copyright (C) 2014 ToadKing
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Server emulator for *.available.gs.nintendowifi.net
and *.master.gs.nintendowifi.net
Query and Reporting:
http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Overview
"""
import logging
import socket
@@ -29,13 +34,10 @@ import other.utils as utils
import traceback
from multiprocessing.managers import BaseManager
import dwc_config
logger = dwc_config.get_logger('GameSpyNatNegServer')
# Logger settings
logger_output_to_console = True
logger_output_to_file = True
logger_name = "GameSpyNatNegServer"
logger_filename = "gamespy_natneg_server.log"
logger = utils.create_logger(logger_name, logger_filename, -1, logger_output_to_console, logger_output_to_file)
class GameSpyServerDatabase(BaseManager):
pass
@@ -49,26 +51,34 @@ GameSpyServerDatabase.register("add_natneg_server")
GameSpyServerDatabase.register("get_natneg_server")
GameSpyServerDatabase.register("delete_natneg_server")
class GameSpyNatNegServer(object):
def __init__(self):
self.session_list = {}
self.natneg_preinit_session = {}
self.secret_key_list = gs_utils.generate_secret_keys("gslist.cfg")
self.server_manager = GameSpyServerDatabase(address=("127.0.0.1", 27500), authkey="")
self.server_manager = GameSpyServerDatabase(
address=dwc_config.get_ip_port('GameSpyManager'),
authkey=""
)
self.server_manager.connect()
def start(self):
try:
# Start natneg server
address = ('0.0.0.0', 27901) # accessible to outside connections (use this if you don't know what you're doing)
# Accessible to outside connections (use this if you don't know
# what you're doing)
address = dwc_config.get_ip_port('GameSpyNatNegServer')
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.socket.bind(address)
self.write_queue = Queue.Queue()
logger.log(logging.INFO, "Server is now listening on %s:%s..." % (address[0], address[1]))
logger.log(logging.INFO,
"Server is now listening on %s:%s...",
address[0], address[1])
threading.Thread(target=self.write_queue_worker).start()
while True:
@@ -76,7 +86,9 @@ class GameSpyNatNegServer(object):
self.handle_packet(recv_data, addr)
except:
logger.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
logger.log(logging.ERROR,
"Unknown exception: %s",
traceback.format_exc())
def write_queue_send(self, data, address):
time.sleep(0.05)
@@ -85,11 +97,17 @@ class GameSpyNatNegServer(object):
def write_queue_worker(self):
while True:
data, address = self.write_queue.get()
threading.Thread(target=self.write_queue_send, args=(data, address)).start()
threading.Thread(target=self.write_queue_send,
args=(data, address)).start()
self.write_queue.task_done()
def handle_packet(self, recv_data, addr):
logger.log(logging.DEBUG, "Connection from %s:%d..." % (addr[0], addr[1]))
"""Handle NATNEG.
TODO: Pointer to methods for recv_data[7]."""
logger.log(logging.DEBUG,
"Connection from %s:%d...",
addr[0], addr[1])
logger.log(logging.DEBUG, utils.pretty_print_hex(recv_data))
# Make sure it's a legal packet
@@ -101,11 +119,15 @@ class GameSpyNatNegServer(object):
# Handle commands
if recv_data[7] == '\x00':
logger.log(logging.DEBUG, "Received initialization from %s:%s..." % (addr[0], addr[1]))
logger.log(logging.DEBUG,
"Received initialization from %s:%s...",
addr[0], addr[1])
output = bytearray(recv_data[0:14])
output += bytearray([0xff, 0xff, 0x6d, 0x16, 0xb5, 0x7d, 0xea ]) # Checked with Tetris DS, Mario Kart DS, and Metroid Prime Hunters, and this seems to be the standard response to 0x00
output[7] = 0x01 # Initialization response
# Checked with Tetris DS, Mario Kart DS, and Metroid Prime
# Hunters, and this seems to be the standard response to 0x00
output += bytearray([0xff, 0xff, 0x6d, 0x16, 0xb5, 0x7d, 0xea])
output[7] = 0x01 # Initialization response
self.write_queue.put((output, addr))
# Try to connect to the server
@@ -120,90 +142,138 @@ class GameSpyNatNegServer(object):
localport = utils.get_short(localport_raw, 0, True)
localaddr = (localip, localport, localip_int_le, localip_int_be)
self.session_list.setdefault(session_id, {}).setdefault(client_id, {
'connected': False,
'addr': '',
'localaddr': None,
'serveraddr': None,
'gameid': None
})
self.session_list \
.setdefault(session_id, {}) \
.setdefault(client_id,
{
'connected': False,
'addr': '',
'localaddr': None,
'serveraddr': None,
'gameid': None
})
# In fact, it's a pointer (cf. shallow copy)
client_id_session = self.session_list[session_id][client_id]
self.session_list[session_id][client_id]['gameid'] = gameid
self.session_list[session_id][client_id]['addr'] = addr
self.session_list[session_id][client_id]['localaddr'] = localaddr
clients = len(self.session_list[session_id])
client_id_session['gameid'] = gameid
client_id_session['addr'] = addr
client_id_session['localaddr'] = localaddr
clients = len(self.session_list[session_id]) # Unused?
for client in self.session_list[session_id]:
if self.session_list[session_id][client]['connected'] == False: # and self.session_list[session_id][client]['localaddr'][1] != 0:
if client == client_id:
continue
# Another shallow copy
client_session = self.session_list[session_id][client]
if client_session['connected'] or client == client_id:
continue
#if self.session_list[session_id][client]['serveraddr'] == None:
serveraddr = self.get_server_info(gameid, session_id, client)
if serveraddr == None:
serveraddr = self.get_server_info_alt(gameid, session_id, client)
# if client_session['serveraddr'] \
# is None:
serveraddr = self.get_server_info(gameid, session_id, client)
if serveraddr is None:
serveraddr = self.get_server_info_alt(
gameid, session_id, client
)
self.session_list[session_id][client]['serveraddr'] = serveraddr
logger.log(logging.DEBUG, "Found server from local ip/port: %s from %d" % (serveraddr, session_id))
client_session['serveraddr'] = serveraddr
logger.log(logging.DEBUG,
"Found server from local ip/port: %s from %d",
serveraddr, session_id)
publicport = self.session_list[session_id][client]['addr'][1]
if self.session_list[session_id][client]['localaddr'][1] != 0:
publicport = self.session_list[session_id][client]['localaddr'][1]
publicport = client_session['addr'][1]
if not client_session['localaddr'][1]:
publicport = client_session['localaddr'][1]
if self.session_list[session_id][client]['serveraddr'] != None:
publicport = int(self.session_list[session_id][client]['serveraddr']['publicport'])
if client_session['serveraddr'] is not None:
publicport = int(
client_session['serveraddr']['publicport']
)
# Send to requesting client
output = bytearray(recv_data[0:12])
output += bytearray([int(x) for x in self.session_list[session_id][client]['addr'][0].split('.')])
output += utils.get_bytes_from_short(publicport, True)
# Send to requesting client
output = bytearray(recv_data[0:12])
output += bytearray([
int(x) for x in client_session['addr'][0].split('.')
])
output += utils.get_bytes_from_short(publicport, True)
output += bytearray([0x42, 0x00]) # Unknown, always seems to be \x42\x00
output[7] = 0x05
#self.write_queue.put((output, (self.session_list[session_id][client_id]['addr'])))
self.write_queue.put((output, (self.session_list[session_id][client_id]['addr'][0], self.session_list[session_id][client_id]['addr'][1])))
# Unknown, always seems to be \x42\x00
output += bytearray([0x42, 0x00])
output[7] = 0x05
# self.write_queue.put((
# output,
# (client_id_session['addr'])
# ))
self.write_queue.put((
output,
(client_id_session['addr'][0],
client_id_session['addr'][1])
))
logger.log(logging.DEBUG, "Sent connection request to %s:%d..." % (self.session_list[session_id][client_id]['addr'][0], self.session_list[session_id][client_id]['addr'][1]))
logger.log(logging.DEBUG, utils.pretty_print_hex(output))
logger.log(logging.DEBUG,
"Sent connection request to %s:%d...",
client_id_session['addr'][0],
client_id_session['addr'][1])
logger.log(logging.DEBUG, '%s', utils.pretty_print_hex(output))
# Send to other client
#if self.session_list[session_id][client_id]['serveraddr'] == None:
serveraddr = self.get_server_info(gameid, session_id, client_id)
if serveraddr == None:
serveraddr = self.get_server_info_alt(gameid, session_id, client_id)
# Send to other client
# if client_id_session['serveraddr'] is None:
serveraddr = self.get_server_info(
gameid, session_id, client_id
)
if serveraddr is None:
serveraddr = self.get_server_info_alt(
gameid, session_id, client_id
)
self.session_list[session_id][client_id]['serveraddr'] = serveraddr
logger.log(logging.DEBUG, "Found server 2 from local ip/port: %s from %d" % (serveraddr, session_id))
client_id_session['serveraddr'] = serveraddr
logger.log(logging.DEBUG,
"Found server 2 from local ip/port: %s from %d",
serveraddr, session_id)
publicport = self.session_list[session_id][client_id]['addr'][1]
if self.session_list[session_id][client_id]['localaddr'][1] != 0:
publicport = self.session_list[session_id][client_id]['localaddr'][1]
publicport = client_id_session['addr'][1]
if client_id_session['localaddr'][1]:
publicport = client_id_session['localaddr'][1]
if self.session_list[session_id][client_id]['serveraddr'] != None:
publicport = int(self.session_list[session_id][client_id]['serveraddr']['publicport'])
if client_id_session['serveraddr'] is not None:
publicport = int(
client_id_session['serveraddr']['publicport']
)
output = bytearray(recv_data[0:12])
output += bytearray([int(x) for x in self.session_list[session_id][client_id]['addr'][0].split('.')])
output += utils.get_bytes_from_short(publicport, True)
output = bytearray(recv_data[0:12])
output += bytearray(
[int(x) for x in client_id_session['addr'][0].split('.')]
)
output += utils.get_bytes_from_short(publicport, True)
output += bytearray([0x42, 0x00]) # Unknown, always seems to be \x42\x00
output[7] = 0x05
#self.write_queue.put((output, (self.session_list[session_id][client]['addr'])))
self.write_queue.put((output, (self.session_list[session_id][client]['addr'][0], self.session_list[session_id][client]['addr'][1])))
# Unknown, always seems to be \x42\x00
output += bytearray([0x42, 0x00])
output[7] = 0x05
# self.write_queue.put((output, (client_session['addr'])))
self.write_queue.put((output, (client_session['addr'][0],
client_session['addr'][1])))
logger.log(logging.DEBUG, "Sent connection request to %s:%d..." % (self.session_list[session_id][client]['addr'][0], self.session_list[session_id][client]['addr'][1]))
logger.log(logging.DEBUG, utils.pretty_print_hex(output))
logger.log(logging.DEBUG,
"Sent connection request to %s:%d...",
client_session['addr'][0],
client_session['addr'][1])
logger.log(logging.DEBUG,
'%s',
utils.pretty_print_hex(output))
elif recv_data[7] == '\x06': # Was able to connect
elif recv_data[7] == '\x06': # Was able to connect
client_id = "%02x" % ord(recv_data[13])
logger.log(logging.DEBUG, "Received connected command from %s:%s..." % (addr[0], addr[1]))
logger.log(logging.DEBUG,
"Received connected command from %s:%s...",
addr[0], addr[1])
if session_id in self.session_list and client_id in self.session_list[session_id]:
if session_id in self.session_list and \
client_id in self.session_list[session_id]:
self.session_list[session_id][client_id]['connected'] = True
elif recv_data[7] == '\x0a': # Address check. Note: UNTESTED!
elif recv_data[7] == '\x0a': # Address check. Note: UNTESTED!
client_id = "%02x" % ord(recv_data[13])
logger.log(logging.DEBUG, "Received address check command from %s:%s..." % (addr[0], addr[1]))
logger.log(logging.DEBUG,
"Received address check command from %s:%s...",
addr[0], addr[1])
output = bytearray(recv_data[0:15])
output += bytearray([int(x) for x in addr[0].split('.')])
@@ -213,53 +283,66 @@ class GameSpyNatNegServer(object):
output[7] = 0x0b
self.write_queue.put((output, addr))
logger.log(logging.DEBUG, "Sent address check response to %s:%d..." % (addr[0], addr[1]))
logger.log(logging.DEBUG, utils.pretty_print_hex(output))
logger.log(logging.DEBUG,
"Sent address check response to %s:%d...",
addr[0], addr[1])
logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))
elif recv_data[7] == '\x0c': # Natify
elif recv_data[7] == '\x0c': # Natify
port_type = "%02x" % ord(recv_data[12])
logger.log(logging.DEBUG, "Received natify command from %s:%s..." % (addr[0], addr[1]))
logger.log(logging.DEBUG,
"Received natify command from %s:%s...",
addr[0], addr[1])
output = bytearray(recv_data)
output[7] = 0x02 # ERT Test
output[7] = 0x02 # ERT Test
self.write_queue.put((output, addr))
logger.log(logging.DEBUG, "Sent natify response to %s:%d..." % (addr[0], addr[1]))
logger.log(logging.DEBUG, utils.pretty_print_hex(output))
logger.log(logging.DEBUG,
"Sent natify response to %s:%d...",
addr[0], addr[1])
logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))
elif recv_data[7] == '\x0d': # Report
logger.log(logging.DEBUG, "Received report command from %s:%s..." % (addr[0], addr[1]))
logger.log(logging.DEBUG, utils.pretty_print_hex(recv_data))
elif recv_data[7] == '\x0d': # Report
logger.log(logging.DEBUG,
"Received report command from %s:%s...",
addr[0], addr[1])
logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))
# Report response
output = bytearray(recv_data[:21])
output[7] = 0x0e # Report response
output[14] = 0 # Clear byte to match real server's response
output[7] = 0x0e # Report response
output[14] = 0 # Clear byte to match real server's response
self.write_queue.put((output, addr))
elif recv_data[7] == '\x0f':
# Natneg v4 command thanks to Pipian.
# Only seems to be used in very few DS games (namely, Pokemon Black/White/Black 2/White 2).
logger.log(logging.DEBUG, "Received pre-init command from %s:%s..." % (addr[0], addr[1]))
logger.log(logging.DEBUG, utils.pretty_print_hex(recv_data))
# Only seems to be used in very few DS games (namely,
# Pokemon Black/White/Black 2/White 2).
logger.log(logging.DEBUG,
"Received pre-init command from %s:%s...",
addr[0], addr[1])
logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))
session = utils.get_int(recv_data[-4:], 0)
# Report response
output = bytearray(recv_data[:-4]) + bytearray([0, 0, 0, 0])
output[7] = 0x10 # Pre-init response
output[7] = 0x10 # Pre-init response
if session == 0:
if not session:
# What's the correct behavior when session == 0?
output[13] = 2
elif session in self.natneg_preinit_session:
# Should this be sent to both clients or just the one that connected most recently?
# Should this be sent to both clients or just the one that
# connected most recently?
# I can't tell from a one sided packet capture of Pokemon.
# For the time being, send to both clients just in case.
output[13] = 2
self.write_queue.put((output, self.natneg_preinit_session[session]))
self.write_queue.put((output,
self.natneg_preinit_session[session]))
output[12] = (1, 0)[output[12]] # Swap the index
output[12] = (1, 0)[output[12]] # Swap the index
del self.natneg_preinit_session[session]
else:
output[13] = 0
@@ -267,28 +350,39 @@ class GameSpyNatNegServer(object):
self.write_queue.put((output, addr))
else: # Was able to connect
logger.log(logging.DEBUG, "Received unknown command %02x from %s:%s..." % (ord(recv_data[7]), addr[0], addr[1]))
else: # Was able to connect
logger.log(logging.DEBUG,
"Received unknown command %02x from %s:%s...",
ord(recv_data[7]), addr[0], addr[1])
def get_server_info(self, gameid, session_id, client_id):
server_info = None
servers = self.server_manager.get_natneg_server(session_id)._getvalue()
servers = self.server_manager.get_natneg_server(session_id) \
._getvalue()
if servers == None:
if servers is None:
return None
console = False
ipstr = self.session_list[session_id][client_id]['addr'][0]
ip = str(utils.get_ip(bytearray([int(x) for x in ipstr.split('.')]), 0, console))
ip = str(utils.get_ip(bytearray(
[int(x) for x in ipstr.split('.')]
), 0, console))
console = not console
server_info = next((s for s in servers if s['publicip'] == ip), None)
if server_info == None:
ip = str(utils.get_ip(bytearray([int(x) for x in ipstr.split('.')]), 0, console))
if server_info is None:
ip = str(utils.get_ip(
bytearray([int(x) for x in ipstr.split('.')]),
0, console
))
server_info = next((s for s in servers if s['publicip'] == ip), None)
server_info = next(
(s for s in servers if s['publicip'] == ip),
None
)
return server_info
@@ -296,19 +390,33 @@ class GameSpyNatNegServer(object):
console = False
ipstr = self.session_list[session_id][client_id]['addr'][0]
ip = str(utils.get_ip(bytearray([int(x) for x in ipstr.split('.')]), 0, console))
ip = str(utils.get_ip(
bytearray([int(x) for x in ipstr.split('.')]),
0, console
))
console = not console
serveraddr = self.server_manager.find_server_by_local_address(ip, self.session_list[session_id][client_id]['localaddr'], self.session_list[session_id][client_id]['gameid'])._getvalue()
serveraddr = self.server_manager.find_server_by_local_address(
ip,
self.session_list[session_id][client_id]['localaddr'],
self.session_list[session_id][client_id]['gameid']
)._getvalue()
if serveraddr == None:
ip = str(utils.get_ip(bytearray([int(x) for x in ipstr.split('.')]), 0, console))
if serveraddr is None:
ip = str(utils.get_ip(
bytearray([int(x) for x in ipstr.split('.')]),
0, console
))
serveraddr = self.server_manager.find_server_by_local_address(ip, self.session_list[session_id][client_id]['localaddr'],
self.session_list[session_id][client_id]['gameid'])._getvalue()
serveraddr = self.server_manager.find_server_by_local_address(
ip,
self.session_list[session_id][client_id]['localaddr'],
self.session_list[session_id][client_id]['gameid']
)._getvalue()
return serveraddr
if __name__ == "__main__":
natneg_server = GameSpyNatNegServer()
natneg_server.start()

View File

@@ -1,19 +1,22 @@
# DWC Network Server Emulator
# Copyright (C) 2014 polaris-
# Copyright (C) 2014 msoucy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""DWC Network Server Emulator
Copyright (C) 2014 polaris-
Copyright (C) 2014 msoucy
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging
import traceback
@@ -27,26 +30,25 @@ from twisted.internet.error import ReactorAlreadyRunning
import gamespy.gs_database as gs_database
import gamespy.gs_query as gs_query
import other.utils as utils
import dwc_config
# Logger settings
logger_output_to_console = True
logger_output_to_file = True
logger_name = "GameSpyPlayerSearchServer"
logger_filename = "gamespy_player_search_server.log"
logger = utils.create_logger(logger_name, logger_filename, -1, logger_output_to_console, logger_output_to_file)
logger = dwc_config.get_logger('GameSpyPlayerSearchServer')
address = dwc_config.get_ip_port('GameSpyPlayerSearchServer')
address = ("0.0.0.0", 29901)
class GameSpyPlayerSearchServer(object):
def __init__(self):
pass
def start(self):
endpoint_search = serverFromString(reactor, "tcp:%d:interface=%s" % (address[1], address[0]))
endpoint_search = serverFromString(
reactor,
"tcp:%d:interface=%s" % (address[1], address[0])
)
conn_search = endpoint_search.listen(PlayerSearchFactory())
try:
if reactor.running == False:
if not reactor.running:
reactor.run(installSignalHandlers=0)
except ReactorAlreadyRunning:
pass
@@ -54,7 +56,9 @@ class GameSpyPlayerSearchServer(object):
class PlayerSearchFactory(Factory):
def __init__(self):
logger.log(logging.INFO, "Now listening for player search connections on %s:%d...", address[0], address[1])
logger.log(logging.INFO,
"Now listening for player search connections on %s:%d...",
address[0], address[1])
def buildProtocol(self, address):
return PlayerSearch(address)
@@ -76,7 +80,7 @@ class PlayerSearch(LineReceiver):
def rawDataReceived(self, data):
try:
logger.log(logging.DEBUG, "SEARCH RESPONSE: %s" % data)
logger.log(logging.DEBUG, "SEARCH RESPONSE: %s", data)
data = self.leftover + data
commands, self.leftover = gs_query.parse_gamespy_message(data)
@@ -87,15 +91,45 @@ class PlayerSearch(LineReceiver):
if data_parsed['__cmd__'] == "otherslist":
self.perform_otherslist(data_parsed)
else:
logger.log(logging.DEBUG, "Found unknown search command, don't know how to handle '%s'." % data_parsed['__cmd__'])
logger.log(logging.DEBUG,
"Found unknown search command, don't know"
" how to handle '%s'.",
data_parsed['__cmd__'])
except:
logger.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
logger.log(logging.ERROR,
"Unknown exception: %s",
traceback.format_exc())
def perform_otherslist(self, data_parsed):
# Reference: http://wiki.tockdom.com/wiki/MKWii_Network_Protocol/Server/gpsp.gs.nintendowifi.net
# Example from: filtered-mkw-log-2014-01-01-ct1310.eth
# \otherslist\\o\146376154\uniquenick\2m0isbjmvRMCJ2i5321j\o\192817284\uniquenick\1jhggtmghRMCJ2jrsh23\o\302594991\uniquenick\7dkjp51v5RMCJ2nr3vs9\o\368031897\uniquenick\1v7p3qmkpRMCJ1o8f56p\o\447214276\uniquenick\7dkt0p6gtRMCJ2ljh72h\o\449615791\uniquenick\4puvrm1g4RMCJ00ho3v1\o\460250854\uniquenick\4rik5l1u1RMCJ0tc3fii\o\456284963\uniquenick\1unitvi86RMCJ1b10u02\o\453830866\uniquenick\7de3q52dbRMCJ2877ss2\o\450197498\uniquenick\3qtutr1ikRMCJ38gem1n\o\444241868\uniquenick\67tp53bs9RMCJ1abs7ej\o\420030955\uniquenick\5blesqia3RMCJ322bbd6\o\394609454\uniquenick\0hddp7mq2RMCJ30uv7r7\o\369478991\uniquenick\59de9c2bhRMCJ0re0fii\o\362755626\uniquenick\5tte2lif7RMCJ0cscgtg\o\350951571\uniquenick\7aeummjlaRMCJ3li4ls2\o\350740680\uniquenick\484uiqhr4RMCJ18opoj0\o\349855648\uniquenick\5blesqia3RMCJ1c245dn\o\324078642\uniquenick\62go5gpt0RMCJ0v0uhc9\o\304111337\uniquenick\4lcg6ampvRMCJ1gjre51\o\301273266\uniquenick\1dhdpjhn8RMCJ2da6f9h\o\193178453\uniquenick\3pcgu0299RMCJ3nhu50f\o\187210028\uniquenick\3tau15a9lRMCJ2ar247h\o\461622261\uniquenick\59epddrnkRMCJ1t2ge7l\oldone\\final\
"""Reference: http://wiki.tockdom.com/wiki/MKWii_Network_Protocol/Server/gpsp.gs.nintendowifi.net
Example from: filtered-mkw-log-2014-01-01-ct1310.eth
\otherslist\\o\146376154\uniquenick\2m0isbjmvRMCJ2i5321j
\o\192817284\uniquenick\1jhggtmghRMCJ2jrsh23
\o\302594991\uniquenick\7dkjp51v5RMCJ2nr3vs9
\o\368031897\uniquenick\1v7p3qmkpRMCJ1o8f56p
\o\447214276\uniquenick\7dkt0p6gtRMCJ2ljh72h
\o\449615791\uniquenick\4puvrm1g4RMCJ00ho3v1
\o\460250854\uniquenick\4rik5l1u1RMCJ0tc3fii
\o\456284963\uniquenick\1unitvi86RMCJ1b10u02
\o\453830866\uniquenick\7de3q52dbRMCJ2877ss2
\o\450197498\uniquenick\3qtutr1ikRMCJ38gem1n
\o\444241868\uniquenick\67tp53bs9RMCJ1abs7ej
\o\420030955\uniquenick\5blesqia3RMCJ322bbd6
\o\394609454\uniquenick\0hddp7mq2RMCJ30uv7r7
\o\369478991\uniquenick\59de9c2bhRMCJ0re0fii
\o\362755626\uniquenick\5tte2lif7RMCJ0cscgtg
\o\350951571\uniquenick\7aeummjlaRMCJ3li4ls2
\o\350740680\uniquenick\484uiqhr4RMCJ18opoj0
\o\349855648\uniquenick\5blesqia3RMCJ1c245dn
\o\324078642\uniquenick\62go5gpt0RMCJ0v0uhc9
\o\304111337\uniquenick\4lcg6ampvRMCJ1gjre51
\o\301273266\uniquenick\1dhdpjhn8RMCJ2da6f9h
\o\193178453\uniquenick\3pcgu0299RMCJ3nhu50f
\o\187210028\uniquenick\3tau15a9lRMCJ2ar247h
\o\461622261\uniquenick\59epddrnkRMCJ1t2ge7l
\oldone\\final\
"""
msg_d = [
('__cmd__', "otherslist"),
('__cmd_val__', ""),
@@ -104,15 +138,18 @@ class PlayerSearch(LineReceiver):
if "numopids" in data_parsed and "opids" in data_parsed:
numopids = int(data_parsed['numopids'])
opids = data_parsed['opids'].split('|')
if (len(opids) != numopids) and (not int(opids[0]) == 0):
logger.log(logging.ERROR, "Unexpected number of opids, got %d, expected %d." % (len(opids), numopids))
if len(opids) != numopids and int(opids[0]):
logger.log(logging.ERROR,
"Unexpected number of opids, got %d, expected %d.",
len(opids), numopids)
# Return all uniquenicks despite any unexpected/missing opids
# We can do better than that, I think...
for opid in opids:
profile = self.db.get_profile_from_profileid(opid)
msg_d.append(('o', opid))
if profile != None:
if profile is not None:
msg_d.append(('uniquenick', profile['uniquenick']))
else:
msg_d.append(('uniquenick', ''))
@@ -120,10 +157,10 @@ class PlayerSearch(LineReceiver):
msg_d.append(('oldone', ""))
msg = gs_query.create_gamespy_message(msg_d)
logger.log(logging.DEBUG, "SENDING: %s" % msg)
logger.log(logging.DEBUG, "SENDING: %s", msg)
self.transport.write(bytes(msg))
if __name__ == "__main__":
gsps = GameSpyPlayerSearchServer()
gsps.start()

View File

@@ -1,21 +1,24 @@
# DWC Network Server Emulator
# Copyright (C) 2014 polaris-
# Copyright (C) 2014 ToadKing
# Copyright (C) 2014 AdmiralCurtiss
# Copyright (C) 2014 msoucy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""DWC Network Server Emulator
Copyright (C) 2014 polaris-
Copyright (C) 2014 ToadKing
Copyright (C) 2014 AdmiralCurtiss
Copyright (C) 2014 msoucy
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging
import time
@@ -31,48 +34,57 @@ import gamespy.gs_database as gs_database
import gamespy.gs_query as gs_query
import gamespy.gs_utility as gs_utils
import other.utils as utils
import dwc_config
logger = dwc_config.get_logger('GameSpyProfileServer')
address = dwc_config.get_ip_port('GameSpyProfileServer')
# Logger settings
logger_output_to_console = True
logger_output_to_file = True
logger_name = "GameSpyProfileServer"
logger_filename = "gamespy_profile_server.log"
logger = utils.create_logger(logger_name, logger_filename, -1, logger_output_to_console, logger_output_to_file)
address = ("0.0.0.0", 29900)
class GameSpyProfileServer(object):
def __init__(self):
pass
def start(self):
endpoint = serverFromString(reactor, "tcp:%d:interface=%s" % (address[1], address[0]))
endpoint = serverFromString(
reactor,
"tcp:%d:interface=%s" % (address[1], address[0])
)
conn = endpoint.listen(PlayerFactory())
try:
if reactor.running == False:
if not reactor.running:
reactor.run(installSignalHandlers=0)
except ReactorAlreadyRunning:
pass
class PlayerFactory(Factory):
def __init__(self):
# Instead of storing the sessions in the database, it might make more sense to store them in the PlayerFactory.
logger.log(logging.INFO, "Now listening for connections on %s:%d...", address[0], address[1])
"""Player Factory.
Instead of storing the sessions in the database, it might make more
sense to store them in the PlayerFactory.
"""
logger.log(logging.INFO,
"Now listening for connections on %s:%d...",
address[0], address[1])
self.sessions = {}
def buildProtocol(self, address):
return PlayerSession(self.sessions, address)
class PlayerSession(LineReceiver):
def __init__(self, sessions, address):
self.setRawMode() # We're dealing with binary data so set to raw mode
self.setRawMode() # We're dealing with binary data so set to raw mode
self.db = gs_database.GamespyDatabase()
self.sessions = sessions
self.address = address
self.remaining_message = "" # Stores any unparsable/incomplete commands until the next rawDataReceived
# Stores any unparsable/incomplete commands until the next
# rawDataReceived
self.remaining_message = ""
self.profileid = 0
self.gameid = ""
@@ -89,22 +101,30 @@ class PlayerSession(LineReceiver):
self.sdkrevision = "0"
def log(self, level, message):
if self.profileid == 0:
if self.gameid == "":
logger.log(level, "[%s:%d] %s", self.address.host, self.address.port,message)
def log(self, level, msg, *args, **kwargs):
if not self.profileid:
if not self.gameid:
logger.log(level, "[%s:%d] " + msg,
self.address.host, self.address.port,
*args, **kwargs)
else:
logger.log(level, "[%s:%d | %s] %s", self.address.host, self.address.port, self.gameid, message)
logger.log(level, "[%s:%d | %s] " + msg,
self.address.host, self.address.port, self.gameid,
*args, **kwargs)
else:
if self.gameid == "":
logger.log(level, "[%s:%d | %d] %s", self.address.host, self.address.port, self.profileid, message)
if not self.gameid:
logger.log(level, "[%s:%d | %d] " + msg,
self.address.host, self.address.port,
self.profileid, *args, **kwargs)
else:
logger.log(level, "[%s:%d | %d | %s] %s", self.address.host, self.address.port, self.profileid, self.gameid, message)
logger.log(level, "[%s:%d | %d | %s] " + msg,
self.address.host, self.address.port,
self.profileid, self.gameid, *args, **kwargs)
def get_ip_as_int(self, address):
ipaddress = 0
if address != None:
if address is not None:
for n in address.split('.'):
ipaddress = (ipaddress << 8) | int(n)
@@ -114,15 +134,20 @@ class PlayerSession(LineReceiver):
try:
self.transport.setTcpKeepAlive(1)
self.log(logging.INFO, "Received connection from %s:%d" % (self.address.host, self.address.port))
self.log(logging.INFO,
"Received connection from %s:%d",
self.address.host, self.address.port)
# Create new session id
self.session = ""
# Generate a random challenge string
self.challenge = utils.generate_random_str(10, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
self.challenge = utils.generate_random_str(
10, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
# The first command sent to the client is always a login challenge containing the server challenge key.
# The first command sent to the client is always a login challenge
# containing the server challenge key.
msg = gs_query.create_gamespy_message([
('__cmd__', "lc"),
('__cmd_val__', "1"),
@@ -130,14 +155,16 @@ class PlayerSession(LineReceiver):
('id', "1"),
])
self.log(logging.DEBUG, "SENDING: '%s'..." % msg)
self.log(logging.DEBUG, "SENDING: '%s'...", msg)
self.transport.write(bytes(msg))
except:
self.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
self.log(logging.ERROR,
"Unknown exception: %s",
traceback.format_exc())
def connectionLost(self, reason):
try:
self.log(logging.INFO, "Client disconnected")
self.log(logging.INFO, "%s", "Client disconnected")
self.status = "0"
self.statstring = "Offline"
@@ -148,28 +175,35 @@ class PlayerSession(LineReceiver):
del self.sessions[self.profileid]
self.db.delete_session(self.sesskey)
self.log(logging.INFO, "Deleted session " + self.session)
self.log(logging.INFO, "Deleted session %s", self.session)
except:
self.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
self.log(logging.ERROR,
"Unknown exception: %s",
traceback.format_exc())
def rawDataReceived(self, data):
try:
self.log(logging.DEBUG, "RESPONSE: '%s'..." % data)
self.log(logging.DEBUG, "RESPONSE: '%s'...", data)
# In the case where command string is too big to fit into one read, any parts that could not be successfully
# parsed are stored in the variable remaining_message. On the next rawDataReceived command, the remaining
# message and the data are combined to create a full command.
# In the case where command string is too big to fit into one
# read, any parts that could not be successfully parsed are
# stored in the variable remaining_message. On the next
# rawDataReceived command, the remaining message and the data
# are combined to create a full command.
data = self.remaining_message + data
# Check to make sure the data buffer starts with a valid command.
if len(data) > 0 and data[0] != '\\':
# There is data in the buffer but it doesn't start with a \ so there's no chance of it being valid.
# Look for the first instance of \final\ and remove everything before it.
# If \final\ is not in the command string then ignore it.
# There is data in the buffer but it doesn't start with a \ so
# there's no chance of it being valid. Look for the first
# instance of \final\ and remove everything before it. If
# \final\ is not in the command string then ignore it.
final = "\\final\\"
data = data[data.index(final) + len(final):] if final in data else ""
data = data[data.index(final) + len(final):] \
if final in data else ""
commands, self.remaining_message = gs_query.parse_gamespy_message(data)
commands, self.remaining_message = \
gs_query.parse_gamespy_message(data)
cmds = {
"login": self.perform_login,
@@ -183,28 +217,36 @@ class PlayerSession(LineReceiver):
"delbuddy": self.perform_delbuddy,
"authadd": self.perform_authadd,
}
def cmd_err(data_parsed):
# Maybe write unknown commands to a separate file later so new data can be collected more easily?
self.log(logging.ERROR, "Found unknown command, don't know how to handle '%s'." % data_parsed['__cmd__'])
# Maybe write unknown commands to a separate file later so
# new data can be collected more easily?
self.log(logging.ERROR,
"Found unknown command, don't know how to handle"
" '%s'.", data_parsed['__cmd__'])
for data_parsed in commands:
#self.log(-1, data_parsed)
self.log(logging.DEBUG, data_parsed)
# self.log(-1, data_parsed)
self.log(logging.DEBUG, "%s", data_parsed)
cmds.get(data_parsed['__cmd__'], cmd_err)(data_parsed)
except:
self.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
self.log(logging.ERROR,
"Unknown exception: %s",
traceback.format_exc())
def perform_login(self, data_parsed):
authtoken_parsed = gs_utils.parse_authtoken(data_parsed['authtoken'], self.db)
if authtoken_parsed == None:
self.log(logging.WARNING, "Invalid Authtoken.")
authtoken_parsed = gs_utils.parse_authtoken(data_parsed['authtoken'],
self.db)
if authtoken_parsed is None:
self.log(logging.WARNING, "%s", "Invalid Authtoken.")
msg = gs_query.create_gamespy_message([
('__cmd__', "error"),
('__cmd_val__', ""),
('err', '266'),
('fatal', ''),
('errmsg', 'There was an error validating the pre-authentication.'),
('errmsg', 'There was an error validating the'
' pre-authentication.'),
('id', data_parsed['id']),
])
self.transport.write(bytes(msg))
@@ -214,17 +256,35 @@ class PlayerSession(LineReceiver):
self.sdkrevision = data_parsed['sdkrevision']
# Verify the client's response
valid_response = gs_utils.generate_response(self.challenge, authtoken_parsed['challenge'], data_parsed['challenge'], data_parsed['authtoken'])
valid_response = gs_utils.generate_response(
self.challenge,
authtoken_parsed['challenge'],
data_parsed['challenge'],
data_parsed['authtoken']
)
if data_parsed['response'] != valid_response:
self.log(logging.ERROR, "ERROR: Got invalid response. Got %s, expected %s" % (data_parsed['response'], valid_response))
self.log(logging.ERROR,
"ERROR: Got invalid response."
" Got %s, expected %s",
data_parsed['response'], valid_response)
proof = gs_utils.generate_proof(self.challenge, authtoken_parsed['challenge'], data_parsed['challenge'], data_parsed['authtoken'])
proof = gs_utils.generate_proof(
self.challenge,
authtoken_parsed['challenge'],
data_parsed['challenge'],
data_parsed['authtoken']
)
userid, profileid, gsbrcd, uniquenick = gs_utils.login_profile_via_parsed_authtoken(authtoken_parsed, self.db)
userid, profileid, gsbrcd, uniquenick = \
gs_utils.login_profile_via_parsed_authtoken(authtoken_parsed,
self.db)
if profileid != None:
# Successfully logged in or created account, continue creating session.
loginticket = gs_utils.base64_encode(utils.generate_random_str(16))
if profileid is not None:
# Successfully logged in or created account, continue
# creating session.
loginticket = gs_utils.base64_encode(
utils.generate_random_str(16)
)
self.sesskey = self.db.create_session(profileid, loginticket)
self.sessions[profileid] = self
@@ -232,13 +292,11 @@ class PlayerSession(LineReceiver):
self.buddies = self.db.get_buddy_list(self.profileid)
self.blocked = self.db.get_blocked_list(self.profileid)
if self.sdkrevision == "11": # Used in Tatsunoko vs Capcom
if self.sdkrevision == "11": # Used in Tatsunoko vs Capcom
def make_list(data):
list = []
for d in data:
if d['status'] == 1:
list.append(str(d['buddyProfileId']))
return list
return [str(d['buddyProfileId'])
for d in data
if d['status'] == 1]
block_list = make_list(self.blocked)
msg = gs_query.create_gamespy_message([
@@ -247,7 +305,7 @@ class PlayerSession(LineReceiver):
('list', ','.join(block_list)),
])
self.log(logging.DEBUG, "SENDING: %s" % msg)
self.log(logging.DEBUG, "SENDING: %s", msg)
self.transport.write(bytes(msg))
buddy_list = make_list(self.buddies)
@@ -257,10 +315,9 @@ class PlayerSession(LineReceiver):
('list', ','.join(buddy_list)),
])
self.log(logging.DEBUG, "SENDING: %s" % msg)
self.log(logging.DEBUG, "SENDING: %s", msg)
self.transport.write(bytes(msg))
msg = gs_query.create_gamespy_message([
('__cmd__', "lc"),
('__cmd_val__', "2"),
@@ -269,38 +326,44 @@ class PlayerSession(LineReceiver):
('userid', userid),
('profileid', profileid),
('uniquenick', uniquenick),
# Some kind of token... don't know it gets used or generated, but it doesn't seem to have any negative effects if it's not properly generated.
# Some kind of token... don't know it gets used or generated,
# but it doesn't seem to have any negative effects if it's
# not properly generated.
('lt', loginticket),
('id', data_parsed['id']),
])
# Take the first 4 letters of gsbrcd instead of gamecd because they should be consistent across game
# regions. For example, the US version of Metroid Prime Hunters has the gamecd "AMHE" and the first 4 letters
# of gsbrcd are "AMHE". However, the Japanese version of Metroid Prime Hunters has the gamecd "AMHJ" with
# the first 4 letters of bsbrcd as "AMHE". Tetris DS is the other way, with the first 4 letters as the
# Japanese version (ATRJ) while the gamecd is region specific (ATRE for US and ATRJ for JP).
# gameid is used to send all people on the player's friends list a status updates, so don't make it region
# specific.
# Take the first 4 letters of gsbrcd instead of gamecd because
# they should be consistent across game regions. For example, the
# US version of Metroid Prime Hunters has the gamecd "AMHE" and
# the first 4 letters of gsbrcd are "AMHE". However, the Japanese
# version of Metroid Prime Hunters has the gamecd "AMHJ" with the
# first 4 letters of bsbrcd as "AMHE". Tetris DS is the other way,
# with the first 4 letters as the Japanese version (ATRJ) while
# the gamecd is region specific (ATRE for US and ATRJ for JP).
# gameid is used to send all people on the player's friends list a
# status updates, so don't make it region specific.
self.gameid = gsbrcd[:4]
self.profileid = int(profileid)
self.log(logging.DEBUG, "SENDING: %s" % msg)
self.log(logging.DEBUG, "SENDING: %s", msg)
self.transport.write(bytes(msg))
# Get pending messages.
self.get_pending_messages()
# Send any friend statuses when the user logs in.
# This will allow the user to see if their friends are hosting a game as soon as they log in.
# This will allow the user to see if their friends are hosting a
# game as soon as they log in.
self.get_status_from_friends()
self.send_status_to_friends()
# profile = self.db.get_profile_from_profileid(profileid)
# if profile != None:
# if profile is not None:
# self.statstring = profile['stat']
# self.locstring = profile['loc']
else:
self.log(logging.INFO, "Invalid password or banned user")
self.log(logging.INFO, "%s", "Invalid password or banned user")
msg = gs_query.create_gamespy_message([
('__cmd__', "error"),
('__cmd_val__', ""),
@@ -309,11 +372,13 @@ class PlayerSession(LineReceiver):
('errmsg', 'Login failed.'),
('id', data_parsed['id']),
])
self.log(logging.DEBUG, "SENDING: %s" % msg)
self.log(logging.DEBUG, "SENDING: %s", msg)
self.transport.write(bytes(msg))
def perform_logout(self, data_parsed):
self.log(logging.INFO, "Session %s has logged off" % (data_parsed['sesskey']))
self.log(logging.INFO,
"Session %s has logged off",
data_parsed['sesskey'])
self.db.delete_session(data_parsed['sesskey'])
if self.profileid in self.sessions:
@@ -322,10 +387,16 @@ class PlayerSession(LineReceiver):
self.transport.loseConnection()
def perform_getprofile(self, data_parsed):
#profile = self.db.get_profile_from_session_key(data_parsed['sesskey'])
# profile = self.db.get_profile_from_session_key(
# data_parsed['sesskey']
# )
profile = self.db.get_profile_from_profileid(data_parsed['profileid'])
# Wii example: \pi\\profileid\474888031\nick\5pde5vhn1WR9E2g1t533\userid\442778352\email\5pde5vhn1WR9E2g1t533@nds\sig\b126556e5ee62d4da9629dfad0f6b2a8\uniquenick\5pde5vhn1WR9E2g1t533\pid\11\lon\0.000000\lat\0.000000\loc\\id\2\final\
# Wii example:
# \pi\\profileid\474888031\nick\5pde5vhn1WR9E2g1t533\userid\442778352
# \email\5pde5vhn1WR9E2g1t533@nds\sig\b126556e5ee62d4da9629dfad0f6b2a8
# \uniquenick\5pde5vhn1WR9E2g1t533\pid\11\lon\0.000000\lat\0.000000
# \loc\\id\2\final\
sig = utils.generate_random_hex_str(32)
msg_d = [
@@ -340,10 +411,11 @@ class PlayerSession(LineReceiver):
('pid', profile['pid']),
]
if profile['firstname'] != "":
msg_d.append(('firstname', profile['firstname'])) # Wii gets a firstname
if profile['firstname']:
# Wii gets a firstname
msg_d.append(('firstname', profile['firstname']))
if profile['lastname'] != "":
if profile['lastname']:
msg_d.append(('lastname', profile['lastname']))
msg_d.extend([
@@ -354,14 +426,18 @@ class PlayerSession(LineReceiver):
])
msg = gs_query.create_gamespy_message(msg_d)
self.log(logging.DEBUG, "SENDING: %s" % msg)
self.log(logging.DEBUG, "SENDING: %s", msg)
self.transport.write(bytes(msg))
def perform_updatepro(self, data_parsed):
# Wii example: \updatepro\\sesskey\199714190\firstname\Wii:2555151656076614@WR9E\partnerid\11\final\
"""Wii example:
\updatepro\\sesskey\199714190\firstname\Wii:2555151656076614@WR9E
\partnerid\11\final\
# Remove any fields not related to what we should be updating.
# To avoid any crashes, make sure the key is actually in the dictionary before removing it.
Remove any fields not related to what we should be updating.
To avoid any crashes, make sure the key is actually in the dictionary
before removing it.
"""
if "__cmd__" in data_parsed:
data_parsed.pop('__cmd__')
if "__cmd_val__" in data_parsed:
@@ -377,7 +453,6 @@ class PlayerSession(LineReceiver):
for f in data_parsed:
self.db.update_profile(self.profileid, (f, data_parsed[f]))
def perform_ka(self, data_parsed):
self.keepalive = int(time.time())
@@ -387,7 +462,6 @@ class PlayerSession(LineReceiver):
])
self.transport.write(msg)
def perform_status(self, data_parsed):
self.sesskey = data_parsed['sesskey']
self.status = data_parsed['__cmd_val__']
@@ -402,9 +476,9 @@ class PlayerSession(LineReceiver):
self.send_status_to_friends()
def perform_bm(self, data_parsed):
if data_parsed['__cmd_val__'] in ("1", "5", "102", "103"): # Message to/from clients?
# Message to/from clients?
if data_parsed['__cmd_val__'] in ("1", "5", "102", "103"):
if "t" in data_parsed:
# Send message to the profile id in "t"
dest_profileid = int(data_parsed['t'])
@@ -413,7 +487,8 @@ class PlayerSession(LineReceiver):
not_buddies = False
# Check if the user is buddies with the target user before sending message.
# Check if the user is buddies with the target user before
# sending message.
if not_buddies:
for buddy in self.buddies:
if buddy['userProfileId'] == dest_profileid:
@@ -426,16 +501,21 @@ class PlayerSession(LineReceiver):
not_buddies = True
break
# Send error to user if they tried to send a message to someone who isn't a buddy.
# Send error to user if they tried to send a message to
# someone who isn't a buddy.
if not_buddies:
msg = gs_query.create_gamespy_message([
('__cmd__', "error"),
('__cmd_val__', ""),
('err', 2305),
('errmsg', "The profile the message was to be sent to is not a buddy."),
('errmsg',
"The profile the message was to be sent to is not"
" a buddy."),
('id', 1),
])
logger.log(logging.DEBUG, "Trying to send message to someone who isn't a buddy: %s" % msg)
logger.log(logging.DEBUG,
"Trying to send message to someone who isn't"
" a buddy: %s", msg)
self.transport.write(msg)
return
@@ -447,33 +527,44 @@ class PlayerSession(LineReceiver):
])
if dest_profileid in self.sessions:
self.log(logging.DEBUG, "SENDING TO %s:%s: %s" % (self.sessions[dest_profileid].address.host, self.sessions[dest_profileid].address.port, msg))
self.log(logging.DEBUG,
"SENDING TO %s:%s: %s",
self.sessions[dest_profileid].address.host,
self.sessions[dest_profileid].address.port, msg)
self.sessions[dest_profileid].transport.write(bytes(msg))
self.send_status_to_friends(dest_profileid)
self.get_status_from_friends(dest_profileid)
else:
if data_parsed['__cmd_val__'] == "1":
self.log(logging.DEBUG, "Saving message to %d: %s" % (dest_profileid, msg))
self.db.save_pending_message(self.profileid, dest_profileid, msg)
self.log(logging.DEBUG,
"Saving message to %d: %s",
dest_profileid, msg)
self.db.save_pending_message(self.profileid,
dest_profileid, msg)
else:
msg = gs_query.create_gamespy_message([
('__cmd__', "error"),
('__cmd_val__', ""),
('err', 2307),
('errmsg', "The buddy to send a message to is offline."),
('errmsg', "The buddy to send a message to"
" is offline."),
('id', 1),
])
logger.log(logging.DEBUG, "Trying to send message to someone who isn't online: %s" % msg)
logger.log(logging.DEBUG,
"Trying to send message to someone who"
" isn't online: %s", msg)
self.transport.write(msg)
def perform_addbuddy(self, data_parsed):
newprofileid = int(data_parsed['newprofileid'])
if newprofileid == self.profileid:
logger.log(logging.DEBUG, "Can't add self as friend: %d == %d", newprofileid, self.profileid)
logger.log(logging.DEBUG,
"Can't add self as friend: %d == %d",
newprofileid, self.profileid)
return
# Sample: \addbuddy\\sesskey\231601763\newprofileid\476756820\reason\\final\
# Sample:
# \addbuddy\\sesskey\231601763\newprofileid\476756820\reason\\final\
self.buddies = self.db.get_buddy_list(self.profileid)
buddy_exists = False
@@ -482,46 +573,58 @@ class PlayerSession(LineReceiver):
buddy_exists = True
break
if buddy_exists == False:
if not buddy_exists:
self.db.add_buddy(self.profileid, newprofileid)
if newprofileid in self.sessions:
logger.log(logging.DEBUG, "User is online, sending direct request from profile id %d to profile id %d..." % (self.profileid, newprofileid))
# TODO: Add a way to check if a profile id is already a buddy using SQL
logger.log(logging.DEBUG,
"User is online, sending direct request from"
" profile id %d to profile id %d...",
self.profileid, newprofileid)
# TODO: Add a way to check if a profile id is already a buddy
# using SQL
other_player_authorized = False
target_buddy_list = self.db.get_buddy_list(newprofileid)
logger.log(logging.DEBUG, target_buddy_list)
logger.log(logging.DEBUG, "%s", target_buddy_list)
for buddy in target_buddy_list:
if buddy['buddyProfileId'] == self.profileid and buddy['blocked'] == 0:
if buddy['buddyProfileId'] == self.profileid and \
not buddy['blocked']:
other_player_authorized = True
break
if other_player_authorized == True:
logger.log(logging.DEBUG, "Automatic authorization: %d (target) already has %d (source) as a friend." % (newprofileid, self.profileid))
if other_player_authorized:
logger.log(logging.DEBUG,
"Automatic authorization: %d (target) already"
" has %d (source) as a friend.",
newprofileid, self.profileid)
# Force them both to add each other
self.send_buddy_request(self.sessions[newprofileid], self.profileid)
self.send_buddy_request(self.sessions[self.profileid], newprofileid)
self.send_buddy_request(self.sessions[newprofileid],
self.profileid)
self.send_buddy_request(self.sessions[self.profileid],
newprofileid)
self.send_bm4(newprofileid)
self.db.auth_buddy(newprofileid, self.profileid)
self.db.auth_buddy(self.profileid, newprofileid)
self.send_status_to_friends(newprofileid)
self.get_status_from_friends(newprofileid)
else:
self.send_buddy_request(self.sessions[newprofileid], self.profileid)
self.send_buddy_request(self.sessions[newprofileid],
self.profileid)
else:
# Trying to add someone who is already a friend. Just send status updates
# Trying to add someone who is already a friend.
# Just send status updates.
self.send_status_to_friends(newprofileid)
self.get_status_from_friends(newprofileid)
self.buddies = self.db.get_buddy_list(self.profileid)
def send_bm4(self, playerid):
def send_bm4(self, playerid):
date = int(time.time())
msg = gs_query.create_gamespy_message([
('__cmd__', "bm"),
@@ -530,36 +633,46 @@ class PlayerSession(LineReceiver):
('date', date),
('msg', ""),
])
self.transport.write(bytes(msg))
def perform_delbuddy(self, data_parsed):
# Sample: \delbuddy\\sesskey\61913621\delprofileid\1\final\
"""Sample:
\delbuddy\\sesskey\61913621\delprofileid\1\final\
"""
self.db.delete_buddy(self.profileid, int(data_parsed['delprofileid']))
self.buddies = self.db.get_buddy_list(self.profileid)
def perform_authadd(self, data_parsed):
# Sample: \authadd\\sesskey\231587549\fromprofileid\217936895\sig\f259f26d3273f8bda23c7c5e4bd8c5aa\final\
# Authorize the other person's friend request.
"""Authorize the other person's friend request.
Sample:
\authadd\\sesskey\231587549\fromprofileid\217936895
\sig\f259f26d3273f8bda23c7c5e4bd8c5aa\final\
"""
target_profile = int(data_parsed['fromprofileid'])
self.db.auth_buddy(target_profile, self.profileid)
self.get_buddy_authorized()
self.buddies = self.db.get_buddy_list(self.profileid)
self.send_bm4(target_profile)
self.send_status_to_friends(target_profile)
self.get_status_from_friends(target_profile)
def send_status_to_friends(self, buddy_profileid = None):
# TODO: Cache buddy list so we don't have to query the database every time
def send_status_to_friends(self, buddy_profileid=None):
"""TODO: Cache buddy list so we don't have to query the
database every time."""
self.buddies = self.db.get_buddy_list(self.profileid)
if self.status == "0" and self.statstring == "Offline":
# Going offline, don't need to send the other information.
status_msg = "|s|%s|ss|%s" % (self.status, self.statstring)
else:
status_msg = "|s|%s|ss|%s|ls|%s|ip|%d|p|0|qm|0" % (self.status, self.statstring, self.locstring, self.get_ip_as_int(self.address.host))
status_msg = "|s|%s|ss|%s|ls|%s|ip|%d|p|0|qm|0" % (
self.status, self.statstring, self.locstring,
self.get_ip_as_int(self.address.host)
)
msg = gs_query.create_gamespy_message([
('__cmd__', "bm"),
@@ -569,29 +682,49 @@ class PlayerSession(LineReceiver):
])
buddy_list = self.buddies
if buddy_profileid != None:
buddy_list = [{"buddyProfileId":buddy_profileid}]
if buddy_profileid is not None:
buddy_list = [{"buddyProfileId": buddy_profileid}]
for buddy in buddy_list:
if buddy['buddyProfileId'] in self.sessions:
#self.log(logging.DEBUG, "Sending status to buddy id %s (%s:%d): %s" % (str(buddy['buddyProfileId']), self.sessions[buddy['buddyProfileId']].address.host, self.sessions[buddy['buddyProfileId']].address.port, msg))
self.sessions[buddy['buddyProfileId']].transport.write(bytes(msg))
# self.log(logging.DEBUG,
# "Sending status to buddy id %s (%s:%d): %s",
# str(buddy['buddyProfileId']),
# self.sessions[
# buddy['buddyProfileId']
# ].address.host,
# self.sessions[
# buddy['buddyProfileId']
# ].address.port, msg)
self.sessions[buddy['buddyProfileId']].transport \
.write(bytes(msg))
def get_status_from_friends(self, buddy_profileid = None):
# This will be called when the player logs in. Grab the player's buddy list and check the current sessions to
# see if anyone is online. If they are online, make them send an update to the calling client.
def get_status_from_friends(self, buddy_profileid=None):
"""This will be called when the player logs in.
Grab the player's buddy list and check the current sessions to
see if anyone is online. If they are online, make them send an update
to the calling client.
"""
self.buddies = self.db.get_buddy_list(self.profileid)
buddy_list = self.buddies
if buddy_profileid != None:
buddy_list = [{"buddyProfileId":buddy_profileid}]
if buddy_profileid is not None:
buddy_list = [{"buddyProfileId": buddy_profileid}]
for buddy in self.buddies:
if buddy['status'] != 1:
continue
if buddy['buddyProfileId'] in self.sessions and self.sessions[buddy['buddyProfileId']].gameid == self.gameid:
status_msg = "|s|%s|ss|%s|ls|%s|ip|%d|p|0|qm|0" % (self.sessions[buddy['buddyProfileId']].status, self.sessions[buddy['buddyProfileId']].statstring, self.sessions[buddy['buddyProfileId']].locstring, self.get_ip_as_int(self.sessions[buddy['buddyProfileId']].address.host))
if buddy['buddyProfileId'] in self.sessions and \
self.sessions[buddy['buddyProfileId']].gameid == self.gameid:
status_msg = "|s|%s|ss|%s|ls|%s|ip|%d|p|0|qm|0" % (
self.sessions[buddy['buddyProfileId']].status,
self.sessions[buddy['buddyProfileId']].statstring,
self.sessions[buddy['buddyProfileId']].locstring,
self.get_ip_as_int(self.sessions[
buddy['buddyProfileId']
].address.host))
else:
status_msg = "|s|0|ss|Offline"
@@ -612,25 +745,30 @@ class PlayerSession(LineReceiver):
('__cmd__', "bm"),
('__cmd_val__', "1"),
('f', buddy['userProfileId']),
('msg', "I have authorized your request to add me to your list"),
('msg', "I have authorized your request to add me to"
" your list"),
])
self.transport.write(bytes(msg))
self.db.buddy_sent_auth_message(buddy['userProfileId'], buddy['buddyProfileId'])
self.db.buddy_sent_auth_message(buddy['userProfileId'],
buddy['buddyProfileId'])
def get_buddy_requests(self):
# Get list people who have added the user but haven't been accepted yet.
"""Get list people who have added the user but haven't been accepted
yet."""
buddies = self.db.get_pending_buddy_requests(self.profileid)
for buddy in buddies:
self.send_buddy_request(self, buddy['userProfileId'], buddy['time'])
self.send_buddy_request(self,
buddy['userProfileId'],
buddy['time'])
def send_buddy_request(self, session, profileid, senttime = None):
def send_buddy_request(self, session, profileid, senttime=None):
sig = utils.generate_random_hex_str(32)
msg = "\r\n\r\n"
msg += "|signed|" + sig
if senttime == None:
if senttime is None:
senttime = int(time.time())
msg = gs_query.create_gamespy_message([
@@ -653,6 +791,7 @@ class PlayerSession(LineReceiver):
except:
self.transport.write(bytearray(message['msg'], "utf-8"))
if __name__ == "__main__":
gsps = GameSpyProfileServer()
gsps.start()

View File

@@ -1,23 +1,28 @@
# DWC Network Server Emulator
# Copyright (C) 2014 polaris-
# Copyright (C) 2014 ToadKing
# Copyright (C) 2014 AdmiralCurtiss
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""DWC Network Server Emulator
# Server emulator for *.available.gs.nintendowifi.net and *.master.gs.nintendowifi.net
# Query and Reporting: http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Overview
Copyright (C) 2014 polaris-
Copyright (C) 2014 ToadKing
Copyright (C) 2014 AdmiralCurtiss
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Server emulator for *.available.gs.nintendowifi.net and
*.master.gs.nintendowifi.net
Query and Reporting:
http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Overview
"""
import logging
import select
@@ -33,24 +38,22 @@ from multiprocessing.managers import BaseManager
import gamespy.gs_utility as gs_utils
import gamespy.gs_database as gs_database
import other.utils as utils
import dwc_config
from gamespy_server_browser_server import GameSpyServerBrowserServer
# Logger settings
logger_output_to_console = True
logger_output_to_file = True
logger_name = "GameSpyQRServer"
logger_filename = "gamespy_qr_server.log"
logger = utils.create_logger(logger_name, logger_filename, -1, logger_output_to_console, logger_output_to_file)
logger = dwc_config.get_logger('GameSpyQRServer')
class GameSpyServerDatabase(BaseManager):
pass
class GameSpyQRServer(object):
class Session(object):
def __init__(self, address):
self.session = ""
self.challenge = ""
self.secretkey = "" # Parse gslist.cfg later
self.secretkey = "" # Parse gslist.cfg later
self.sent_challenge = False
self.heartbeat_data = None
self.address = address
@@ -61,47 +64,63 @@ class GameSpyQRServer(object):
self.gamename = ""
self.keepalive = -1
def __init__(self):
self.sessions = {}
# Generate a dictionary "secret_key_list" containing the secret game keys associated with their game IDs.
# The dictionary key will be the game's ID, and the value will be the secret key.
# Generate a dictionary "secret_key_list" containing the secret game
# keys associated with their game IDs. The dictionary key will be the
# game's ID, and the value will be the secret key.
self.secret_key_list = gs_utils.generate_secret_keys("gslist.cfg")
#self.log(logging.DEBUG, address, session_id, "Generated list of secret game keys...")
# self.log(logging.DEBUG, address, session_id,
# "Generated list of secret game keys...")
GameSpyServerDatabase.register("update_server_list")
GameSpyServerDatabase.register("delete_server")
def log(self, level, address, session_id, message):
if address == None:
logger.log(level, "%s", message)
def log(self, level, address, session_id, msg, *args, **kwargs):
"""TODO: Use logger format"""
if address is None:
logger.log(level, msg, *args, **kwargs)
else:
if session_id != None:
logger.log(level, "[%s:%d %08x] %s", address[0], address[1], session_id, message)
if session_id is not None:
logger.log(level, "[%s:%d %08x] " + msg,
address[0], address[1], session_id,
*args, **kwargs)
else:
logger.log(level, "[%s:%d] %s", address[0], address[1], message)
logger.log(level, "[%s:%d] " + msg,
address[0], address[1],
*args, **kwargs)
def start(self):
try:
manager_address = ("127.0.0.1", 27500)
manager_address = dwc_config.get_ip_port('GameSpyManager')
manager_password = ""
self.server_manager = GameSpyServerDatabase(address = manager_address, authkey= manager_password)
self.server_manager = GameSpyServerDatabase(
address=manager_address,
authkey=manager_password
)
self.server_manager.connect()
# Start QR server
address = ('0.0.0.0', 27900) # accessible to outside connections (use this if you don't know what you're doing)
# Accessible to outside connections (use this if you don't know
# what you're doing)
address = dwc_config.get_ip_port('GameSpyQRServer')
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.socket.bind(address)
self.socket.setblocking(0)
logger.log(logging.INFO, "Server is now listening on %s:%s..." % (address[0], address[1]))
logger.log(logging.INFO,
"Server is now listening on %s:%s...",
address[0], address[1])
# Dependencies! I don't really like this solution but it's easier than trying to manage it another way.
# Dependencies! I don't really like this solution but it's easier
# than trying to manage it another way.
server_browser_server = GameSpyServerBrowserServer(self)
server_browser_server_thread = threading.Thread(target=server_browser_server.start)
server_browser_server_thread = threading.Thread(
target=server_browser_server.start
)
server_browser_server_thread.start()
self.write_queue = Queue.Queue()
@@ -117,124 +136,153 @@ class GameSpyQRServer(object):
self.keepalive_check()
except:
logger.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
logger.log(logging.ERROR,
"Unknown exception: %s",
traceback.format_exc())
def write_queue_send(self, data, address):
time.sleep(0.05)
self.socket.sendto(data, address)
def write_queue_worker(self):
while 1:
while True:
data, address = self.write_queue.get()
threading.Thread(target=self.write_queue_send, args=(data, address)).start()
threading.Thread(target=self.write_queue_send,
args=(data, address)).start()
self.write_queue.task_done()
def update_server_list(self, session_id, k):
if "statechanged" in k and k['statechanged'] == "2": # Close server
self.server_manager.delete_server(k['gamename'] , session_id)
if "statechanged" in k and k['statechanged'] == "2": # Close server
self.server_manager.delete_server(k['gamename'], session_id)
if session_id in self.sessions:
del self.sessions[session_id]
if session_id in self.sessions:
del self.sessions[session_id]
else:
# dwc_mtype controls what kind of server query we're looking for.
# dwc_mtype = 0 is used when looking for a matchmaking game.
# dwc_mtype = 1 is unknown.
# dwc_mtype = 2 is used when hosting a friends only game (possibly other uses too).
# dwc_mtype = 3 is used when looking for a friends only game (possibly other uses too).
# dwc_mtype = 2 is used when hosting a friends only game
# (possibly other uses too).
# dwc_mtype = 3 is used when looking for a friends only game
# (possibly other uses too).
# Some memory could be saved by clearing out any unwanted fields from k before sending.
self.server_manager.update_server_list(k['gamename'], session_id, k, self.sessions[session_id].console)._getvalue()
# Some memory could be saved by clearing out any unwanted fields
# from k before sending.
self.server_manager.update_server_list(
k['gamename'], session_id, k,
self.sessions[session_id].console
)._getvalue()
if session_id in self.sessions:
self.sessions[session_id].gamename = k['gamename']
def handle_packet(self, socket, recv_data, address):
# Tetris DS overlay 10 @ 02144184 - Handle responses back to server
# Tetris DS overlay 10 @ 02144184 - Handle responses back to server
#
# After some more packet inspection, it seems the format goes something like this:
# - All server messages seem to always start with \xfe\xfd.
# - The first byte from the client (or third byte from the server) is a command.
# - Bytes 2 - 5 from the client is some kind of ID. This will have to be inspected later. I believe it's a
# session-like ID because the number changes between connections. Copying the client's ID might be enough.
#
# The above was as guessed.
# The code in Tetris DS (overlay 10) @ 0216E974 handles the network command creation.
# R1 contains the command to be sent to the server.
# R2 contains a pointer to some unknown integer that gets written after the command.
#
# - Commands
# Commands range from 0x00 to 0x09 (for client only at least?) (Tetris DS overlay 10 @ 0216DDCC)
#
# CLIENT:
# 0x01 - Response (Tetris DS overlay 10 @ 216DCA4)
# Sends back base64 of RC4 encrypted string that was gotten from the server's 0x01.
#
# 0x03 - Send client state? (Tetris DS overlay 10 @ 216DA30)
# Data sent:
# 1) Loop for each localip available on the system, write as localip%d\x00(local ip)
# 2) localport\x00(local port)
# 3) natneg (either 0 or 1)
# 4) ONLY IF STATE CHANGED: statechanged\x00(state) (Possible values: 0, 1, 2, 3)
# 5) gamename\x00(game name)
# 6) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE: publicip\x00(public ip)
# 7) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE: publicport\x00(public port)
#
# if statechanged != 2:
# Write various other data described here: http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Implementation
#
# 0x07 - Unknown, related to server's 0x06 (returns value sent from server)
#
# 0x08 - Keep alive? Sent after 0x03
#
# 0x09 - Availability check
#
# SERVER:
# 0x01 - Unknown
# Data sent:
# 8 random ASCII characters (?) followed by the public IP and port of the client as a hex string
#
# 0x06 - Unknown
# First 4 bytes is some kind of id? I believe it's a unique identifier for the data being sent,
# seeing how the server can send the same IP information many times in a row. If the IP information has
# already been parsed then it doesn't waste time handling it.
#
# After that is a "SBCM" section which is 0x14 bytes in total.
# SBCM information gets parsed at 2141A0C in Tetris DS overlay 10.
# Seems to contain IP address information.
#
# The SBCM seems to contain a little information that must be parsed before.
# After the SBCM:
# \x03\x00\x00\x00 - Always the same?
# \x01 - Found player?
# \x04 - Unknown
# (2 bytes) - Unknown. Port?
# (4 bytes) - Player's IP
# (4 bytes) - Unknown. Some other IP? Remote server IP?
# \x00\x00\x00\x00 - Unknown but seems to get checked
#
# Another SBCM, after a player has been found and attempting to start a game:
# \x03\x00\x00\x00 - Always the same?
# \x05 - Connecting to player?
# \x00 - Unknown
# (2 bytes) - Unknown. Port? Same as before.
# (4 bytes) - Player's IP
# (4 bytes) - Unknown. Some other IP? Remote server IP?
#
# 0x0a - Response to 0x01
# Gets sent after receiving 0x01 from the client. So, server 0x01 -> client 0x01 -> server 0x0a.
# Has no other data besides the client ID.
#
# - \xfd\xfc commands get passed directly between the other player(s)?
#
#
# Open source version of GameSpy found here: https://github.com/sfcspanky/Openspy-Core/tree/master/qr
# Use as reference.
"""Tetris DS overlay 10 @ 02144184 - Handle responses back to server.
After some more packet inspection, it seems the format goes something
like this:
- All server messages seem to always start with \xfe\xfd.
- The first byte from the client (or third byte from the server) is
a command.
- Bytes 2 - 5 from the client is some kind of ID. This will have to
be inspected later. I believe it's a session-like ID because the
number changes between connections. Copying the client's ID might
be enough.
The above was as guessed.
The code in Tetris DS (overlay 10) @ 0216E974 handles the network
command creation.
R1 contains the command to be sent to the server.
R2 contains a pointer to some unknown integer that gets written
after the command.
- Commands
Commands range from 0x00 to 0x09 (for client only at least?)
(Tetris DS overlay 10 @ 0216DDCC)
CLIENT:
0x01 - Response (Tetris DS overlay 10 @ 216DCA4)
Sends back base64 of RC4 encrypted string that was
gotten from the server's 0x01.
0x03 - Send client state? (Tetris DS overlay 10 @ 216DA30)
Data sent:
1) Loop for each localip available on the system, write
as localip%d\x00(local ip)
2) localport\x00(local port)
3) natneg (either 0 or 1)
4) ONLY IF STATE CHANGED: statechanged\x00(state)
(Possible values: 0, 1, 2, 3)
5) gamename\x00(game name)
6) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE:
publicip\x00(public ip)
7) ONLY IF PUBLIC IP AND PORT ARE AVAILABLE:
publicport\x00(public port)
if statechanged != 2:
Write various other data described here:
http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Implementation
0x07 - Unknown, related to server's 0x06
(returns value sent from server)
0x08 - Keep alive? Sent after 0x03
0x09 - Availability check
SERVER:
0x01 - Unknown
Data sent:
8 random ASCII characters (?) followed by the public IP and
port of the client as a hex string
0x06 - Unknown
First 4 bytes is some kind of id? I believe it's a unique
identifier for the data being sent, seeing how the server
can send the same IP information many times in a row. If
the IP information has already been parsed then it doesn't
waste time handling it.
After that is a "SBCM" section which is 0x14 bytes in
total. SBCM information gets parsed at 2141A0C in Tetris DS
overlay 10. Seems to contain IP address information.
The SBCM seems to contain a little information that must be
parsed before.
After the SBCM:
\x03\x00\x00\x00 - Always the same?
\x01 - Found player?
\x04 - Unknown
(2 bytes) - Unknown. Port?
(4 bytes) - Player's IP
(4 bytes) - Unknown. Some other IP? Remote server IP?
\x00\x00\x00\x00 - Unknown but seems to get checked
Another SBCM, after a player has been found and attempting
to start a game:
\x03\x00\x00\x00 - Always the same?
\x05 - Connecting to player?
\x00 - Unknown
(2 bytes) - Unknown. Port? Same as before.
(4 bytes) - Player's IP
(4 bytes) - Unknown. Some other IP? Remote server IP?
0x0a - Response to 0x01
Gets sent after receiving 0x01 from the client.
So, server 0x01 -> client 0x01 -> server 0x0a.
Has no other data besides the client ID.
- \xfd\xfc commands get passed directly between the other player(s)?
Open source version of GameSpy found here:
https://github.com/sfcspanky/Openspy-Core/tree/master/qr
Use as reference.
"""
session_id = None
if recv_data[0] != '\x09':
# Don't add a session if the client is trying to check if the game is available or not
# Don't add a session if the client is trying to check if the game
# is available or not
session_id = struct.unpack("<I", recv_data[1:5])[0]
session_id_raw = recv_data[1:5]
if session_id not in self.sessions:
@@ -244,78 +292,117 @@ class GameSpyQRServer(object):
self.sessions[session_id].keepalive = int(time.time())
self.sessions[session_id].disconnected = False
if session_id in self.sessions and self.sessions[session_id].disconnected == True:
if session_id in self.sessions and \
self.sessions[session_id].disconnected:
return
if session_id in self.sessions:
self.sessions[session_id].keepalive = int(time.time()) # Make sure the server doesn't get removed
# Make sure the server doesn't get removed
self.sessions[session_id].keepalive = int(time.time())
# Handle commands
if recv_data[0] == '\x00': # Query
self.log(logging.DEBUG, address, session_id, "NOT IMPLEMENTED! Received query from %s:%s... %s" % (address[0], address[1], recv_data[5:]))
if recv_data[0] == '\x00': # Query
self.log(logging.DEBUG, address, session_id,
"NOT IMPLEMENTED! Received query from %s:%s... %s",
address[0], address[1], recv_data[5:])
elif recv_data[0] == '\x01': # Challenge
self.log(logging.DEBUG, address, session_id, "Received challenge from %s:%s... %s" % (address[0], address[1], recv_data[5:]))
elif recv_data[0] == '\x01': # Challenge
self.log(logging.DEBUG, address, session_id,
"Received challenge from %s:%s... %s",
address[0], address[1], recv_data[5:])
# Prepare the challenge sent from the server to be compared
challenge = gs_utils.prepare_rc4_base64(self.sessions[session_id].secretkey, self.sessions[session_id].challenge)
challenge = gs_utils.prepare_rc4_base64(
self.sessions[session_id].secretkey,
self.sessions[session_id].challenge
)
# Compare challenge
client_challenge = recv_data[5:-1]
if client_challenge == challenge:
# Challenge succeeded
# Send message back to client saying it was accepted
packet = bytearray([0xfe, 0xfd, 0x0a]) # Send client registered command
packet.extend(session_id_raw) # Get the session ID
# Send client registered command
packet = bytearray([0xfe, 0xfd, 0x0a])
packet.extend(session_id_raw) # Get the session ID
self.write_queue.put((packet, address))
self.log(logging.DEBUG, address, session_id, "Sent client registered to %s:%s..." % (address[0], address[1]))
self.log(logging.DEBUG, address, session_id,
"Sent client registered to %s:%s...",
address[0], address[1])
if self.sessions[session_id].heartbeat_data != None:
self.update_server_list(session_id, self.sessions[session_id].heartbeat_data)
if self.sessions[session_id].heartbeat_data is not None:
self.update_server_list(
session_id,
self.sessions[session_id].heartbeat_data
)
else:
# Failed the challenge, request another during the next heartbeat
# Failed the challenge, request another during the next
# heartbeat
self.sessions[session_id].sent_challenge = False
self.server_manager.delete_server(self.sessions[session_id].gamename, session_id)
self.server_manager.delete_server(
self.sessions[session_id].gamename,
session_id
)
elif recv_data[0] == '\x02': # Echo
self.log(logging.DEBUG, address, session_id, "NOT IMPLEMENTED! Received echo from %s:%s... %s" % (address[0], address[1], recv_data[5:]))
elif recv_data[0] == '\x02': # Echo
self.log(logging.DEBUG, address, session_id,
"NOT IMPLEMENTED! Received echo from %s:%s... %s",
address[0], address[1], recv_data[5:])
elif recv_data[0] == '\x03': # Heartbeat
elif recv_data[0] == '\x03': # Heartbeat
data = recv_data[5:]
self.log(logging.DEBUG, address, session_id, "Received heartbeat from %s:%s... %s" % (address[0], address[1], data))
self.log(logging.DEBUG, address, session_id,
"Received heartbeat from %s:%s... %s",
address[0], address[1], data)
# Parse information from heartbeat here
d = data.rstrip('\0').split('\0')
# It may be safe to ignore "unknown" keys because the proper key names get filled in later...
# It may be safe to ignore "unknown" keys because the proper key
# names get filled in later...
k = {}
for i in range(0, len(d), 2):
#self.log(logging.DEBUG, address, session_id, "%s = %s" % (d[i], d[i+1]))
# self.log(logging.DEBUG, address, session_id,
# "%s = %s",
# d[i], d[i + 1])
k[d[i]] = d[i+1]
if self.sessions[session_id].ingamesn != None:
if self.sessions[session_id].ingamesn is not None:
if "gamename" in k and "dwc_pid" in k:
try:
profile = self.db.get_profile_from_profileid(k['dwc_pid'])
naslogin = self.db.get_nas_login_from_userid(profile['userid'])
self.sessions[session_id].ingamesn = str(naslogin['ingamesn']) # convert to string from unicode(which is just a base64 string anyway)
except Exception,e:
pass # If the game doesn't have, don't worry about it.
profile = self.db.get_profile_from_profileid(
k['dwc_pid']
)
naslogin = self.db.get_nas_login_from_userid(
profile['userid']
)
# Convert to string from unicode (which is just a
# base64 string anyway)
self.sessions[session_id].ingamesn = \
str(naslogin['ingamesn'])
except Exception, e:
# If the game doesn't have, don't worry about it.
pass
if self.sessions[session_id].ingamesn != None and "ingamesn" not in k:
if self.sessions[session_id].ingamesn is not None and \
"ingamesn" not in k:
k['ingamesn'] = self.sessions[session_id].ingamesn
if "gamename" in k:
if k['gamename'] in self.secret_key_list:
self.sessions[session_id].secretkey = self.secret_key_list[k['gamename']]
self.sessions[session_id].secretkey = \
self.secret_key_list[k['gamename']]
else:
self.log(logging.INFO, address, session_id, "Connection from unknown game '%s'!" % k['gamename'])
self.log(logging.INFO, address, session_id,
"Connection from unknown game '%s'!",
k['gamename'])
if self.sessions[session_id].playerid == 0 and "dwc_pid" in k:
# Get the player's id and then query the profile to figure out what console they are on.
# The endianness of some server data depends on the endianness of the console, so we must be able
# to account for that.
# Get the player's id and then query the profile to figure
# out what console they are on. The endianness of some server
# data depends on the endianness of the console, so we must be
# able to account for that.
self.sessions[session_id].playerid = int(k['dwc_pid'])
# Try to detect console without hitting the database first
@@ -323,104 +410,169 @@ class GameSpyQRServer(object):
if 'gamename' in k:
self.sessions[session_id].console = 0
if k['gamename'].endswith('ds') or k['gamename'].endswith('dsam') or k['gamename'].endswith('dsi') or k['gamename'].endswith('dsiam'):
if k['gamename'].endswith('ds') or \
k['gamename'].endswith('dsam') or \
k['gamename'].endswith('dsi') or \
k['gamename'].endswith('dsiam'):
self.sessions[session_id].console = 0
found_console = True
elif k['gamename'].endswith('wii') or k['gamename'].endswith('wiiam') or k['gamename'].endswith('wiiware') or k['gamename'].endswith('wiiwaream'):
elif k['gamename'].endswith('wii') or \
k['gamename'].endswith('wiiam') or \
k['gamename'].endswith('wiiware') or \
k['gamename'].endswith('wiiwaream'):
self.sessions[session_id].console = 1
found_console = True
if found_console == False:
if found_console is False:
# Couldn't detect game, try to get it from the database
# Try a 3 times before giving up
for i in range(0, 3):
try:
profile = self.db.get_profile_from_profileid(self.sessions[session_id].playerid)
profile = self.db.get_profile_from_profileid(
self.sessions[session_id].playerid
)
if "console" in profile:
self.sessions[session_id].console = profile['console']
self.sessions[session_id].console = \
profile['console']
break
except:
time.sleep(0.5)
if 'publicip' in k and k['publicip'] == "0": #and k['dwc_hoststate'] == "2": # When dwc_hoststate == 2 then it doesn't send an IP, so calculate it ourselves
if 'publicip' in k and k['publicip'] == "0":
# and k['dwc_hoststate'] == "2":
# When dwc_hoststate == 2 then it doesn't send an IP,
# so calculate it ourselves
be = self.sessions[session_id].console != 0
k['publicip'] = str(utils.get_ip(bytearray([int(x) for x in address[0].split('.')]), 0, be))
k['publicip'] = str(utils.get_ip(
bytearray([int(x) for x in address[0].split('.')]),
0,
be
))
if 'publicport' in k and 'localport' in k and k['publicport'] != k['localport']:
self.log(logging.DEBUG, address, session_id, "publicport %s doesn't match localport %s, so changing publicport to %s..." \
% (k['publicport'], k['localport'], str(address[1])))
if 'publicport' in k and \
'localport' in k and \
k['publicport'] != k['localport']:
self.log(logging.DEBUG, address, session_id,
"publicport %s doesn't match localport %s,"
" so changing publicport to %s...",
k['publicport'], k['localport'],
str(address[1]))
k['publicport'] = str(address[1])
if self.sessions[session_id].sent_challenge == True:
if self.sessions[session_id].sent_challenge:
self.update_server_list(session_id, k)
else:
addr_hex = ''.join(["%02X" % int(x) for x in address[0].split('.')])
addr_hex = ''.join(["%02X" % int(x)
for x in address[0].split('.')])
port_hex = "%04X" % int(address[1])
server_challenge = utils.generate_random_str(6) + '00' + addr_hex + port_hex
server_challenge = utils.generate_random_str(6) + '00' + \
addr_hex + port_hex
self.sessions[session_id].challenge = server_challenge
packet = bytearray([0xfe, 0xfd, 0x01]) # Send challenge command
packet.extend(session_id_raw) # Get the session ID
# Send challenge command
packet = bytearray([0xfe, 0xfd, 0x01])
# Get the session ID
packet.extend(session_id_raw)
packet.extend(server_challenge)
packet.extend('\x00')
self.write_queue.put((packet, address))
self.log(logging.DEBUG, address, session_id, "Sent challenge to %s:%s..." % (address[0], address[1]))
self.log(logging.DEBUG, address, session_id,
"Sent challenge to %s:%s...",
address[0], address[1])
self.sessions[session_id].sent_challenge = True
self.sessions[session_id].heartbeat_data = k
elif recv_data[0] == '\x04': # Add Error
self.log(logging.WARNING, address, session_id, "NOT IMPLEMENTED! Received add error from %s:%s... %s" % (address[0], address[1], recv_data[5:]))
elif recv_data[0] == '\x04': # Add Error
self.log(logging.WARNING, address, session_id,
"NOT IMPLEMENTED! Received add error from %s:%s... %s",
address[0], address[1], recv_data[5:])
elif recv_data[0] == '\x05': # Echo Response
self.log(logging.WARNING, address, session_id, "NOT IMPLEMENTED! Received echo response from %s:%s... %s" % (address[0], address[1], recv_data[5:]))
elif recv_data[0] == '\x05': # Echo Response
self.log(logging.WARNING, address, session_id,
"NOT IMPLEMENTED! Received echo response"
" from %s:%s... %s",
address[0], address[1], recv_data[5:])
elif recv_data[0] == '\x06': # Client Message
self.log(logging.WARNING, address, session_id, "NOT IMPLEMENTED! Received echo from %s:%s... %s" % (address[0], address[1], recv_data[5:]))
elif recv_data[0] == '\x06': # Client Message
self.log(logging.WARNING, address, session_id,
"NOT IMPLEMENTED! Received echo from %s:%s... %s",
address[0], address[1], recv_data[5:])
elif recv_data[0] == '\x07': # Client Message Ack
#self.log(logging.WARNING, address, session_id, "NOT IMPLEMENTED! Received client message ack from %s:%s... %s" % (address[0], address[1], recv_data[5:]))
self.log(logging.DEBUG, address, session_id, "Received client message ack from %s:%s..." % (address[0], address[1]))
elif recv_data[0] == '\x07': # Client Message Ack
# self.log(logging.WARNING, address, session_id,
# "NOT IMPLEMENTED! Received client message ack"
# " from %s:%s... %s",
# address[0], address[1], recv_data[5:])
self.log(logging.DEBUG, address, session_id,
"Received client message ack from %s:%s...",
address[0], address[1])
elif recv_data[0] == '\x08': # Keep Alive
self.log(logging.DEBUG, address, session_id, "Received keep alive from %s:%s..." % (address[0], address[1]))
elif recv_data[0] == '\x08': # Keep Alive
self.log(logging.DEBUG, address, session_id,
"Received keep alive from %s:%s...",
address[0], address[1])
self.sessions[session_id].keepalive = int(time.time())
elif recv_data[0] == '\x09': # Available
elif recv_data[0] == '\x09': # Available
# Availability check only sent to *.available.gs.nintendowifi.net
self.log(logging.DEBUG, address, session_id, "Received availability request for '%s' from %s:%s..." % (recv_data[5: -1], address[0], address[1]))
self.write_queue.put((bytearray([0xfe, 0xfd, 0x09, 0x00, 0x00, 0x00, 0x00]), address))
self.log(logging.DEBUG, address, session_id,
"Received availability request for '%s' from %s:%s...",
recv_data[5: -1], address[0], address[1])
self.write_queue.put((
bytearray([0xfe, 0xfd, 0x09, 0x00, 0x00, 0x00, 0x00]),
address
))
elif recv_data[0] == '\x0a': # Client Registered
elif recv_data[0] == '\x0a': # Client Registered
# Only sent to client, never received?
self.log(logging.WARNING, address, session_id, "NOT IMPLEMENTED! Received client registered from %s:%s... %s" % (address[0], address[1], recv_data[5:]))
self.log(logging.WARNING, address, session_id,
"NOT IMPLEMENTED! Received client registered"
" from %s:%s... %s",
address[0], address[1], recv_data[5:])
else:
self.log(logging.ERROR, address, session_id, "Unknown request from %s:%s:" % (address[0], address[1]))
self.log(logging.DEBUG, address, session_id, utils.pretty_print_hex(recv_data))
self.log(logging.ERROR, address, session_id,
"Unknown request from %s:%s:",
address[0], address[1])
self.log(logging.DEBUG, address, session_id,
"%s",
utils.pretty_print_hex(recv_data))
def keepalive_check(self):
#self.log(logging.DEBUG, None, session_id, "Keep alive check on %d sessions" % (len(self.sessions)))
# self.log(logging.DEBUG, None, session_id,
# "Keep alive check on %d sessions",
# len(self.sessions))
pruned = []
now = int(time.time())
for session_id in self.sessions:
delta = now - self.sessions[session_id].keepalive
timeout = 61 # Remove clients that haven't responded in x seconds
timeout = 61 # Remove clients that haven't responded in x seconds
if delta < 0 or delta >= timeout:
pruned.append(session_id)
self.server_manager.delete_server(self.sessions[session_id].gamename, self.sessions[session_id].session)
self.log(logging.DEBUG, None, session_id, "Keep alive check removed %s:%s for game %s. Client hasn't responded in %d seconds." % (self.sessions[session_id].address[0], self.sessions[session_id].address[1], self.sessions[session_id].gamename, delta))
self.server_manager.delete_server(
self.sessions[session_id].gamename,
self.sessions[session_id].session
)
self.log(logging.DEBUG, None, session_id,
"Keep alive check removed %s:%s for game %s."
" Client hasn't responded in %d seconds.",
self.sessions[session_id].address[0],
self.sessions[session_id].address[1],
self.sessions[session_id].gamename,
delta)
for session_id in pruned:
del self.sessions[session_id]
if __name__ == "__main__":
qr_server = GameSpyQRServer()
qr_server.start()

View File

@@ -1,22 +1,26 @@
# DWC Network Server Emulator
# Copyright (C) 2014 polaris-
# Copyright (C) 2014 msoucy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# I found an open source implemention of this exact server I'm trying to emulate here: https://github.com/sfcspanky/Openspy-Core/blob/master/serverbrowsing/
# Use as reference later.
"""DWC Network Server Emulator
Copyright (C) 2014 polaris-
Copyright (C) 2014 msoucy
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
I found an open source implemention of this exact server I'm trying to
emulate here: (use as reference later)
https://github.com/sfcspanky/Openspy-Core/blob/master/serverbrowsing/
"""
import logging
import socket
@@ -30,9 +34,13 @@ from twisted.internet.error import ReactorAlreadyRunning
import gamespy.gs_utility as gs_utils
import other.utils as utils
import dwc_config
from multiprocessing.managers import BaseManager
logger = dwc_config.get_logger('GameSpyServerBrowserServer')
class ServerListFlags:
UNSOLICITED_UDP_FLAG = 1
PRIVATE_IP_FLAG = 2
@@ -43,12 +51,6 @@ class ServerListFlags:
HAS_KEYS_FLAG = 64
HAS_FULL_RULES_FLAG = 128
# Logger settings
logger_output_to_console = True
logger_output_to_file = True
logger_name = "GameSpyServerBrowserServer"
logger_filename = "gamespy_server_browser_server.log"
logger = utils.create_logger(logger_name, logger_filename, -1, logger_output_to_console, logger_output_to_file)
class GameSpyServerDatabase(BaseManager):
pass
@@ -61,26 +63,31 @@ GameSpyServerDatabase.register("add_natneg_server")
GameSpyServerDatabase.register("get_natneg_server")
GameSpyServerDatabase.register("delete_natneg_server")
address = ("0.0.0.0", 28910)
address = dwc_config.get_ip_port('GameSpyServerBrowserServer')
class GameSpyServerBrowserServer(object):
def __init__(self, qr = None):
def __init__(self, qr=None):
self.qr = qr
def start(self):
endpoint = serverFromString(reactor, "tcp:%d:interface=%s" % (address[1], address[0]))
endpoint = serverFromString(
reactor, "tcp:%d:interface=%s" % (address[1], address[0])
)
conn = endpoint.listen(SessionFactory(self.qr))
try:
if reactor.running == False:
if not reactor.running:
reactor.run(installSignalHandlers=0)
except ReactorAlreadyRunning:
pass
class SessionFactory(Factory):
def __init__(self, qr):
logger.log(logging.INFO, "Now listening for connections on %s:%d...", address[0], address[1])
logger.log(logging.INFO,
"Now listening for connections on %s:%d...",
address[0], address[1])
self.secret_key_list = gs_utils.generate_secret_keys("gslist.cfg")
# TODO: Prune server cache at some point
@@ -88,27 +95,34 @@ class SessionFactory(Factory):
self.qr = qr
def buildProtocol(self, address):
return Session(address, self.secret_key_list, self.server_cache, self.qr)
return Session(address, self.secret_key_list, self.server_cache,
self.qr)
class Session(LineReceiver):
def __init__(self, address, secret_key_list, server_cache, qr):
self.setRawMode() # We're dealing with binary data so set to raw mode
self.setRawMode() # We're dealing with binary data so set to raw mode
self.address = address
self.secret_key_list = secret_key_list # Don't waste time parsing every session, so just accept it from the parent
# Don't waste time parsing every session, so just accept it from
# the parent
self.secret_key_list = secret_key_list
self.console = 0
self.server_cache = server_cache
self.qr = qr
self.own_server = None
self.buffer = []
manager_address = ("127.0.0.1", 27500)
manager_address = dwc_config.get_ip_port('GameSpyManager')
manager_password = ""
self.server_manager = GameSpyServerDatabase(address = manager_address, authkey= manager_password)
self.server_manager = GameSpyServerDatabase(address=manager_address,
authkey=manager_password)
self.server_manager.connect()
def log(self, level, message):
logger.log(level, "[%s:%d] %s", self.address.host, self.address.port,message)
def log(self, level, msg, *args, **kwargs):
"""TODO: Use logger format"""
logger.log(level, "[%s:%d] " + msg,
self.address.host, self.address.port,
*arg, **kwarg)
def rawDataReceived(self, data):
try:
@@ -123,7 +137,8 @@ class Session(LineReceiver):
# 0x04 - Map loop request (?)
# 0x05 - Player search request
#
# For Tetris DS, at the very least 0x00 and 0x02 need to be implemented.
# For Tetris DS, at the very least 0x00 and 0x02 need to be
# implemented.
self.buffer += data
@@ -135,14 +150,17 @@ class Session(LineReceiver):
packet = self.buffer[:packet_len]
self.buffer = self.buffer[packet_len:]
if packet == None:
if packet is None:
# Don't have enough for the entire packet, break.
break
if packet[2] == '\x00': # Server list request
self.log(logging.DEBUG, "Received server list request from %s:%s..." % (self.address.host, self.address.port))
if packet[2] == '\x00': # Server list request
self.log(logging.DEBUG,
"Received server list request from %s:%s...",
self.address.host, self.address.port)
# This code is so... not python. The C programmer in me is coming out strong.
# This code is so... not python. The C programmer in me is
# coming out strong.
# TODO: Rewrite this section later?
idx = 3
list_version = ord(packet[idx])
@@ -184,65 +202,109 @@ class Session(LineReceiver):
send_ip = True
if '\\' in fields:
fields = [x for x in fields.split('\\') if x and not x.isspace()]
fields = [x for x in fields.split('\\')
if x and not x.isspace()]
#print "%02x %02x %08x" % (list_version, encoding_version, game_version)
#print "%s" % query_game
#print "%s" % game_name
#print "%s" % challenge
#print "%s" % filter
#print "%s" % fields
# print "%02x %02x %08x" % \
# (list_version, encoding_version, game_version)
# print "%s" % query_game
# print "%s" % game_name
# print "%s" % challenge
# print "%s" % filter
# print "%s" % fields
#print "%08x" % options
#print "%d %08x" % (max_servers, source_ip)
# print "%08x" % options
# print "%d %08x" % (max_servers, source_ip)
self.log(logging.DEBUG, "list version: %02x / encoding version: %02x / game version: %08x / query game: %s / game name: %s / challenge: %s / filter: %s / fields: %s / options: %08x / max servers: %d / source ip: %08x" % (list_version, encoding_version, game_version, query_game, game_name, challenge, filter, fields, options, max_servers, source_ip))
self.log(logging.DEBUG,
"list version: %02x / encoding version: %02x /"
" game version: %08x / query game: %s /"
" game name: %s / challenge: %s / filter: %s /"
" fields: %s / options: %08x / max servers: %d /"
" source ip: %08x" % (
list_version, encoding_version,
game_version, query_game,
game_name, challenge, filter,
fields, options, max_servers,
source_ip
))
# Requesting ip and port of client, not server
if filter == "" and fields == "" or send_ip == True:
output = bytearray([int(x) for x in self.address.host.split('.')])
output += utils.get_bytes_from_short(6500, True) # Does this ever change?
if not filter and not fields or send_ip:
output = bytearray(
[int(x) for x in self.address.host.split('.')]
)
# Does this ever change?
output += utils.get_bytes_from_short(6500, True)
enc = gs_utils.EncTypeX()
output_enc = enc.encrypt(self.secret_key_list[game_name], challenge, output)
output_enc = enc.encrypt(
self.secret_key_list[game_name],
challenge,
output
)
self.transport.write(bytes(output_enc))
self.log(logging.DEBUG, "Responding with own IP and game port...")
self.log(logging.DEBUG, utils.pretty_print_hex(output))
self.log(logging.DEBUG,
"%s",
"Responding with own IP and game port...")
self.log(logging.DEBUG,
"%s",
utils.pretty_print_hex(output))
else:
self.find_server(query_game, filter, fields, max_servers, game_name, challenge)
self.find_server(query_game, filter, fields,
max_servers, game_name, challenge)
elif packet[2] == '\x02': # Send message request
elif packet[2] == '\x02': # Send message request
packet_len = utils.get_short(packet, 0, True)
dest_addr = '.'.join(["%d" % ord(x) for x in packet[3:7]])
dest_port = utils.get_short(packet, 7, True) # What's the pythonic way to do this? unpack?
# What's the pythonic way to do this? unpack?
dest_port = utils.get_short(packet, 7, True)
dest = (dest_addr, dest_port)
self.log(logging.DEBUG, "Received send message request from %s:%s to %s:%d... expecting %d byte packet." % (self.address.host, self.address.port, dest_addr, dest_port, packet_len))
self.log(logging.DEBUG, utils.pretty_print_hex(bytearray(packet)))
self.log(logging.DEBUG,
"Received send message request from %s:%s to"
" %s:%d... expecting %d byte packet.",
self.address.host, self.address.port,
dest_addr, dest_port, packet_len)
self.log(logging.DEBUG,
"%s",
utils.pretty_print_hex(bytearray(packet)))
if packet_len == len(packet):
# Contains entire packet, send immediately.
self.forward_data_to_client(packet[9:], dest)
else:
self.log(logging.ERROR, "ERROR: Could not find entire packet.")
self.log(logging.ERROR,
"%s",
"ERROR: Could not find entire packet.")
elif packet[2] == '\x03': # Keep alive reply
self.log(logging.DEBUG, "Received keep alive from %s:%s..." % (self.address.host, self.address.port))
elif packet[2] == '\x03': # Keep alive reply
self.log(logging.DEBUG,
"Received keep alive from %s:%s...",
(self.address.host, self.address.port))
else:
self.log(logging.DEBUG, "Received unknown command (%02x) from %s:%s..." % (ord(packet[2]), self.address.host, self.address.port))
self.log(logging.DEBUG, utils.pretty_print_hex(bytearray(packet)))
self.log(logging.DEBUG,
"Received unknown command (%02x) from %s:%s...",
ord(packet[2]),
self.address.host, self.address.port)
self.log(logging.DEBUG,
"%s",
utils.pretty_print_hex(bytearray(packet)))
except:
self.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
self.log(logging.ERROR,
"Unknown exception: %s",
traceback.format_exc())
def get_game_id(self, data):
game_id = data[5: -1]
return game_id
def get_server_list(self, game, filter, fields, max_count):
results = self.server_manager.find_servers(game, filter, fields, max_count)
results = self.server_manager.find_servers(game, filter, fields,
max_count)
return results
def generate_server_list_header_data(self, address, fields):
@@ -260,8 +322,10 @@ class Session(LineReceiver):
if key_count != len(fields):
# For some reason we didn't get all of the expected data.
self.log(logging.WARNING, "key_count[%d] != len(fields)[%d]" % (key_count, len(fields)))
self.log(logging.WARNING, fields)
self.log(logging.WARNING,
"key_count[%d] != len(fields)[%d]",
key_count, len(fields))
self.log(logging.WARNING, "%s", fields)
# Write the fields
for field in fields:
@@ -269,40 +333,55 @@ class Session(LineReceiver):
return output
def generate_server_list_data(self, address, fields, server_info, finalize = False):
def generate_server_list_data(self, address, fields, server_info,
finalize=False):
output = bytearray()
flags_buffer = bytearray()
if len(server_info) > 0:
# Start server loop here instead of including all of the fields and stuff again
# Start server loop here instead of including all of the fields
# and stuff again
flags = 0
if len(server_info) != 0:
# This condition is always true? Isn't it?
flags |= ServerListFlags.HAS_KEYS_FLAG
if "natneg" in server_info:
flags |= ServerListFlags.CONNECT_NEGOTIATE_FLAG
ip = utils.get_bytes_from_int_signed(int(server_info['publicip']), self.console)
ip = utils.get_bytes_from_int_signed(
int(server_info['publicip']), self.console
)
flags_buffer += ip
flags |= ServerListFlags.NONSTANDARD_PORT_FLAG
if server_info['publicport'] != "0":
flags_buffer += utils.get_bytes_from_short(int(server_info['publicport']), True)
flags_buffer += utils.get_bytes_from_short(
int(server_info['publicport']), True
)
else:
flags_buffer += utils.get_bytes_from_short(int(server_info['localport']), True)
flags_buffer += utils.get_bytes_from_short(
int(server_info['localport']), True
)
if "localip0" in server_info:
# How to handle multiple localips?
flags |= ServerListFlags.PRIVATE_IP_FLAG
flags_buffer += bytearray([int(x) for x in server_info['localip0'].split('.')]) #ip
flags_buffer += bytearray(
[int(x) for x in server_info['localip0'].split('.')]
) # IP
if "localport" in server_info:
flags |= ServerListFlags.NONSTANDARD_PRIVATE_PORT_FLAG
flags_buffer += utils.get_bytes_from_short(int(server_info['localport']), True)
flags_buffer += utils.get_bytes_from_short(
int(server_info['localport']), True
)
flags |= ServerListFlags.ICMP_IP_FLAG
flags_buffer += bytearray([int(x) for x in "0.0.0.0".split('.')])
flags_buffer += bytearray(
[int(x) for x in "0.0.0.0".split('.')]
)
output += bytearray([flags & 0xff])
output += flags_buffer
@@ -311,150 +390,226 @@ class Session(LineReceiver):
# Write data for associated fields
if 'requested' in server_info:
for field in fields:
output += '\xff' + bytearray(server_info['requested'][field]) + '\0'
output += '\xff' + \
bytearray(
server_info['requested'][field]
) + '\0'
return output
def find_server(self, query_game, filter, fields, max_servers, game_name, challenge):
def find_server(self, query_game, filter, fields, max_servers, game_name,
challenge):
def send_encrypted_data(self, challenge, data):
self.log(logging.DEBUG, "Sent server list message to %s:%s..." % (self.address.host, self.address.port))
self.log(logging.DEBUG, utils.pretty_print_hex(data))
self.log(logging.DEBUG,
"Sent server list message to %s:%s...",
self.address.host, self.address.port)
self.log(logging.DEBUG, "%s", utils.pretty_print_hex(data))
# Encrypt data
enc = gs_utils.EncTypeX()
data = enc.encrypt(self.secret_key_list[game_name], challenge, data)
data = enc.encrypt(self.secret_key_list[game_name],
challenge, data)
# Send to client
self.transport.write(bytes(data))
max_packet_length = 256 + 511 + 255 # OpenSpy's max packet length, just go with it for now
# OpenSpy's max packet length, just go with it for now
max_packet_length = 256 + 511 + 255
# Get dictionary from master server list server.
self.log(logging.DEBUG, "Searching for server matching '%s' with the fields '%s'" % (filter, fields))
self.log(logging.DEBUG,
"Searching for server matching '%s' with the fields '%s'",
filter, fields)
self.server_list = self.server_manager.find_servers(query_game, filter, fields, max_servers)._getvalue()
self.server_list = self.server_manager.find_servers(
query_game, filter, fields, max_servers
)._getvalue()
self.log(logging.DEBUG, "Found server(s):")
self.log(logging.DEBUG, self.server_list)
self.log(logging.DEBUG, "%s", "Found server(s):")
self.log(logging.DEBUG, "%s", self.server_list)
if self.server_list == []:
self.server_list.append({})
if not self.server_list:
self.server_list = [{}]
data = self.generate_server_list_header_data(self.address, fields)
for i in range(0, len(self.server_list)):
server = self.server_list[i]
if server and fields and 'requested' in server and server['requested'] == {}:
# If the requested fields weren't found then don't return a server.
# This fixes a bug with Mario Kart DS.
#print "Requested was empty"
if server and fields and 'requested' in server and \
not server['requested']:
# If the requested fields weren't found then don't return
# a server. This fixes a bug with Mario Kart DS.
# print "Requested was empty"
server = {}
if "__console__" in server:
self.console = int(server['__console__'])
# Generate binary server list data
data += self.generate_server_list_data(self.address, fields, server, i >= len(self.server_list))
data += self.generate_server_list_data(
self.address, fields, server, i >= len(self.server_list)
)
if len(data) >= max_packet_length:
send_encrypted_data(self, challenge, data)
data = bytearray()
# if "publicip" in server and "publicport" in server:
# self.server_cache[str(server['publicip']) + str(server['publicport'])] = server
# self.server_cache[str(server['publicip']) + \
# str(server['publicport'])] = server
data += '\0'
data += utils.get_bytes_from_int(0xffffffff)
send_encrypted_data(self, challenge, data)
def find_server_in_cache(self, addr, port, console):
ip = str(utils.get_ip(bytearray([int(x) for x in addr.split('.')]), 0, console))
server = self.server_manager.find_server_by_address(ip, port)._getvalue()
self.log(logging.DEBUG, "find_server_in_cache is returning: %s %s" % (server, ip))
ip = str(utils.get_ip(
bytearray([int(x) for x in addr.split('.')]),
0,
console
))
server = self.server_manager.find_server_by_address(ip,
port)._getvalue()
self.log(logging.DEBUG,
"find_server_in_cache is returning: %s %s",
server, ip)
return server, ip
def forward_data_to_client(self, data, forward_client):
# Find session id of server
# Iterate through the list of servers sent to the client and match by IP and port.
# Is there a better way to determine this information?
if forward_client == None or len(forward_client) != 2:
# Iterate through the list of servers sent to the client and match by
# IP and port. Is there a better way to determine this information?
if forward_client is None or len(forward_client) != 2:
return
server, ip = self.find_server_in_cache(forward_client[0], forward_client[1], self.console)
server, ip = self.find_server_in_cache(forward_client[0],
forward_client[1], self.console)
if server == None:
if server is None:
if self.console == 0:
server, ip = self.find_server_in_cache(forward_client[0], forward_client[1], 1) # Try Wii
server, ip = self.find_server_in_cache(forward_client[0],
forward_client[1],
1) # Try Wii
elif self.console == 1:
server, ip = self.find_server_in_cache(forward_client[0], forward_client[1], 0) # Try DS
server, ip = self.find_server_in_cache(forward_client[0],
forward_client[1],
0) # Try DS
self.log(logging.DEBUG, "find_server_in_cache returned: %s" % server)
self.log(logging.DEBUG, "Trying to send message to %s:%d..." % (forward_client[0], forward_client[1]))
self.log(logging.DEBUG, utils.pretty_print_hex(bytearray(data)))
self.log(logging.DEBUG,
"find_server_in_cache returned: %s",
server)
self.log(logging.DEBUG,
"Trying to send message to %s:%d...",
forward_client[0], forward_client[1])
self.log(logging.DEBUG, "%s", utils.pretty_print_hex(bytearray(data)))
if server == None:
if server is None:
return
self.log(logging.DEBUG, "%s %s" % (ip, server['publicip']))
if server['publicip'] == ip and server['publicport'] == str(forward_client[1]):
self.log(logging.DEBUG, "%s %s", ip, server['publicip'])
if server['publicip'] == ip and \
server['publicport'] == str(forward_client[1]):
if forward_client[1] == 0 and 'localport' in server:
# No public port returned from client, try contacting on the local port.
# No public port returned from client, try contacting on
# the local port.
forward_client = (forward_client[0], int(server['localport']))
# Send command to server to get it to connect to natneg
cookie = int(utils.generate_random_hex_str(8), 16) # Quick and lazy way to get a random 32bit integer. Replace with something else later
# Quick and lazy way to get a random 32bit integer. Replace with
# something else later
cookie = int(utils.generate_random_hex_str(8), 16)
#if (len(data) == 24 and bytearray(data)[0:10] == bytearray([0x53, 0x42, 0x43, 0x4d, 0x03, 0x00, 0x00, 0x00, 0x01, 0x04])) or (len(data) == 40 and bytearray(data)[0:10] == bytearray([0x53, 0x42, 0x43, 0x4d, 0x0b, 0x00, 0x00, 0x00, 0x01, 0x04])):
if self.own_server == None and len(data) >= 16 and bytearray(data)[0:4] in (bytearray([0xbb, 0x49, 0xcc, 0x4d]), bytearray([0x53, 0x42, 0x43, 0x4d])):
# Is the endianness the same between the DS and Wii here? It seems so but I'm not positive.
self_port = utils.get_short(bytearray(data[10:12]), 0, False) # Note to self: Port is little endian here.
# if (len(data) == 24 and bytearray(data)[0:10] == \
# bytearray([0x53, 0x42, 0x43, 0x4d, 0x03,
# 0x00, 0x00, 0x00, 0x01, 0x04])) or \
# (len(data) == 40 and bytearray(data)[0:10] == \
# bytearray([0x53, 0x42, 0x43, 0x4d,
# 0x0b, 0x00, 0x00, 0x00,
# 0x01, 0x04])):
if self.own_server is None and len(data) >= 16 and \
bytearray(data)[0:4] in (bytearray([0xbb, 0x49, 0xcc, 0x4d]),
bytearray([0x53, 0x42, 0x43, 0x4d])):
# Is the endianness the same between the DS and Wii here?
# It seems so but I'm not positive.
# Note to self: Port is little endian here.
self_port = utils.get_short(bytearray(data[10:12]), 0, False)
self_ip = '.'.join(["%d" % x for x in bytearray(data[12:16])])
self.own_server, _ = self.find_server_in_cache(self_ip, self_port, self.console)
self.own_server, _ = self.find_server_in_cache(self_ip,
self_port,
self.console)
if self.own_server == None:
if self.own_server is None:
if self.console == 0:
self.own_server, _ = self.find_server_in_cache(self_ip, self_port, 1) # Try Wii
# Try Wii
self.own_server, _ = self.find_server_in_cache(
self_ip, self_port, 1
)
elif self.console == 1:
self.own_server, _ = self.find_server_in_cache(self_ip, self_port, 0) # Try DS
# Try DS
self.own_server, _ = self.find_server_in_cache(
self_ip, self_port, 0
)
if self.own_server == None:
self.log(logging.DEBUG, "Could not find own server: %s:%d" % (self_ip, self_port))
if self.own_server is None:
self.log(logging.DEBUG,
"Could not find own server: %s:%d",
self_ip, self_port)
else:
self.log(logging.DEBUG, "Found own server: %s" % (self.own_server))
self.log(logging.DEBUG,
"Found own server: %s",
self.own_server)
elif len(data) == 10 and \
bytearray(data)[0:6] == \
bytearray([0xfd, 0xfc, 0x1e, 0x66, 0x6a, 0xb2]):
natneg_session = utils.get_int_signed(data, 6)
self.log(logging.DEBUG,
"Adding %d to natneg server list: %s",
natneg_session, server)
# Store info in backend so we can get it later in natneg
self.server_manager.add_natneg_server(natneg_session, server)
elif len(data) == 10 and bytearray(data)[0:6] == bytearray([0xfd, 0xfc, 0x1e, 0x66, 0x6a, 0xb2]):
natneg_session = utils.get_int_signed(data,6)
self.log(logging.DEBUG, "Adding %d to natneg server list: %s" % (natneg_session, server))
self.server_manager.add_natneg_server(natneg_session, server) # Store info in backend so we can get it later in natneg
if self.own_server is not None:
self.log(logging.DEBUG,
"Adding %d to natneg server list: %s (self)",
natneg_session, self.own_server)
# Store info in backend so we can get it later in natneg
self.server_manager.add_natneg_server(natneg_session,
self.own_server)
if self.own_server != None:
self.log(logging.DEBUG, "Adding %d to natneg server list: %s (self)" % (natneg_session, self.own_server))
self.server_manager.add_natneg_server(natneg_session, self.own_server) # Store info in backend so we can get it later in natneg
# if self.qr != None:
# if self.qr is not None:
# own_server = self.qr.get_own_server()
#
# self.log(logging.DEBUG, "Adding %d to natneg server list: %s" % (natneg_session, own_server))
# self.server_manager.add_natneg_server(natneg_session, own_server) # Store info in backend so we can get it later in natneg
# self.log(logging.DEBUG,
# "Adding %d to natneg server list: %s",
# natneg_session, own_server)
# self.server_manager.add_natneg_server(natneg_session,
# own_server)
output = bytearray([0xfe, 0xfd, 0x06])
output += utils.get_bytes_from_int(server['__session__'])
output += bytearray(utils.get_bytes_from_int(cookie))
output += bytearray(data)
if self.qr != None:
self.log(logging.DEBUG, "Forwarded data to %s:%s through QR server..." % (forward_client[0], forward_client[1]))
if self.qr is not None:
self.log(logging.DEBUG,
"Forwarded data to %s:%s through QR server...",
forward_client[0], forward_client[1])
self.qr.socket.sendto(output, forward_client)
else:
# In case we can't contact the QR server, just try sending the packet directly.
# This isn't standard behavior but it can work in some instances.
self.log(logging.DEBUG, "Forwarded data to %s:%s directly (potential error occurred)..." % (forward_client[0], forward_client[1]))
# In case we can't contact the QR server, just try sending
# the packet directly. This isn't standard behavior but it
# can work in some instances.
self.log(logging.DEBUG,
"Forwarded data to %s:%s directly"
" (potential error occurred)...",
forward_client[0], forward_client[1])
client_s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client_s.sendto(output, forward_client)
if __name__ == "__main__":
server_browser = GameSpyServerBrowserServer()
server_browser.start()

View File

@@ -1,21 +1,25 @@
# DWC Network Server Emulator
# Copyright (C) 2014 polaris-
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# TODO: Seperate gamestats.gs.nintendowifi.net and gamestats2.gs.nintendowifi.net
# TODO: Move gamestats list to database?
"""DWC Network Server Emulator
Copyright (C) 2014 polaris-
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
TODO: Seperate gamestats.gs.nintendowifi.net
and gamestats2.gs.nintendowifi.net
TODO: Move gamestats list to database?
"""
import logging
import urlparse
@@ -28,18 +32,14 @@ import base64
import gamespy.gs_database as gs_database
import gamespy.gs_utility as gs_utils
import other.utils as utils
import dwc_config
logger_output_to_console = True
logger_output_to_file = True
logger_name = "GameStatsServerHttp"
logger_filename = "gamestats_server_http.log"
logger = utils.create_logger(logger_name, logger_filename, -1, logger_output_to_console, logger_output_to_file)
logger = dwc_config.get_logger('GameStatsServerHttp')
address = dwc_config.get_ip_port('GameStatsServerHttp')
#address = ("0.0.0.0", 80)
address = ("127.0.0.1", 9002)
class GameStatsBase(object):
def do_GET(self, conn, key, append_hash, append_text = ""):
def do_GET(self, conn, key, append_hash, append_text=""):
try:
conn.send_response(200)
conn.send_header("Content-type", "text/html")
@@ -52,21 +52,24 @@ class GameStatsBase(object):
ret = ""
if "hash" not in params:
# The token is used in combination with the game's secret key.
# The format of the hash parameter sent from the client is sha1(secret_key + token).
# The format of the hash parameter sent from the client is
# sha1(secret_key + token).
token = utils.generate_random_str(32)
ret = token
else:
# Handle data (generic response for now)
ret += append_text
if append_hash == True:
if append_hash:
h = hashlib.sha1()
h.update(key + base64.urlsafe_b64encode(ret) + key)
ret += h.hexdigest()
conn.wfile.write(ret)
except:
logger.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
logger.log(logging.ERROR,
"Unknown exception: %s",
traceback.format_exc())
def do_POST(self, conn, key):
try:
@@ -79,35 +82,52 @@ class GameStatsBase(object):
conn.wfile.write("")
except:
logger.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
logger.log(logging.ERROR,
"Unknown exception: %s",
traceback.format_exc())
class GameStatsVersion1(GameStatsBase):
def do_GET(self, conn, key):
super(self.__class__, self).do_GET(conn, key, False, "")
class GameStatsVersion2(GameStatsBase):
def do_GET(self, conn, key):
super(self.__class__, self).do_GET(conn, key, True, "")
class GameStatsVersion3(GameStatsBase):
def do_GET(self, conn, key):
super(self.__class__, self).do_GET(conn, key, True, "done")
class GameStatsServer(object):
def start(self):
httpd = GameStatsHTTPServer((address[0], address[1]), GameStatsHTTPServerHandler)
logger.log(logging.INFO, "Now listening for connections on %s:%d...", address[0], address[1])
httpd = GameStatsHTTPServer((address[0], address[1]),
GameStatsHTTPServerHandler)
logger.log(logging.INFO,
"Now listening for connections on %s:%d...",
address[0], address[1])
httpd.serve_forever()
class GameStatsHTTPServer(BaseHTTPServer.HTTPServer):
gamestats_list = [
GameStatsBase,
GameStatsVersion1,
GameStatsVersion2,
GameStatsVersion3
]
def __init__(self, server_address, RequestHandlerClass):
#self.db = gs_database.GamespyDatabase()
# self.db = gs_database.GamespyDatabase()
self.gamelist = self.parse_key_file()
BaseHTTPServer.HTTPServer.__init__(self, server_address, RequestHandlerClass)
BaseHTTPServer.HTTPServer.__init__(self, server_address,
RequestHandlerClass)
def parse_key_file(self, filename = "gamestats.cfg"):
gamestats_list = [GameStatsBase, GameStatsVersion1, GameStatsVersion2, GameStatsVersion3]
def parse_key_file(self, filename="gamestats.cfg"):
gamelist = {}
with open(filename) as config_file:
@@ -120,14 +140,15 @@ class GameStatsHTTPServer(BaseHTTPServer.HTTPServer):
if len(s) != 3:
continue
gamestats = gamestats_list[0]
if int(s[1]) < len(gamestats_list):
gamestats = gamestats_list[int(s[1])]
gamestats = self.gamestats_list[0]
if int(s[1]) < len(self.gamestats_list):
gamestats = self.gamestats_list[int(s[1])]
gamelist[s[0]] = {'key': s[2], 'class': gamestats}
return gamelist
class GameStatsHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def version_string(self):
return "Nintendo Wii (http)"
@@ -137,12 +158,14 @@ class GameStatsHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
if '/' in gameid:
gameid = gameid[:gameid.index('/')]
#logger.log(logging.DEBUG, "Request for '%s': %s", gameid, self.path)
# logger.log(logging.DEBUG, "Request for '%s': %s", gameid, self.path)
if gameid in self.server.gamelist:
game = self.server.gamelist[gameid]['class']()
game.do_GET(self, self.server.gamelist[gameid]['key'])
else:
logger.log(logging.DEBUG, "WARNING: Could not find '%s' in gamestats list", gameid)
logger.log(logging.DEBUG,
"WARNING: Could not find '%s' in gamestats list",
gameid)
default = GameStatsBase()
default.do_GET(self, "", False, "")
@@ -157,6 +180,7 @@ class GameStatsHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
return ret
if __name__ == "__main__":
gamestats = GameStatsServer()
gamestats.start()

View File

@@ -1,19 +1,22 @@
# DWC Network Server Emulator
# Copyright (C) 2014 polaris-
# Copyright (C) 2014 msoucy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""DWC Network Server Emulator
Copyright (C) 2014 polaris-
Copyright (C) 2014 msoucy
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from twisted.web import server, resource
from twisted.internet import reactor
@@ -23,41 +26,62 @@ import time
import datetime
import json
import logging
import other.utils as utils
logger_output_to_console = True
logger_output_to_file = True
logger_name = "InternalStatsServer"
logger_filename = "internal_stats_server.log"
logger = utils.create_logger(logger_name, logger_filename, -1, logger_output_to_console, logger_output_to_file)
import other.utils as utils
import dwc_config
logger = dwc_config.get_logger('InternalStatsServer')
class GameSpyServerDatabase(BaseManager):
pass
GameSpyServerDatabase.register("get_server_list")
class StatsPage(resource.Resource):
"""Servers statistics webpage.
Format attributes:
- header
- row
- footer
"""
isLeaf = True
header = """<html>
<table border='1'>
<tr>
<td>Game ID</td><td># Players</td>
</tr>"""
row = """
<tr>
<td>%s</td>
<td><center>%d</center></td>
</tr>""" # % (game, len(server_list[game]))
footer = """</table>
<br>
<i>Last updated: %s</i><br>
</html>""" # % (self.stats.get_last_update_time())
def __init__(self, stats):
self.stats = stats
def render_GET(self, request):
raw = False
force_update = False
if '/'.join(request.postpath) == "json":
if "/".join(request.postpath) == "json":
raw = True
force_update = True
else:
raw = False
force_update = False
server_list = self.stats.get_server_list(force_update)
if raw == True:
if raw:
# List of keys to be removed
restricted = [ "publicip", "__session__", "localip0", "localip1" ]
restricted = ["publicip", "__session__", "localip0", "localip1"]
# Filter out certain fields before displaying raw data
if server_list != None:
if server_list is not None:
for game in server_list:
for server in server_list[game]:
for r in restricted:
@@ -67,65 +91,61 @@ class StatsPage(resource.Resource):
output = json.dumps(server_list)
else:
output = "<html>"
output += "<table border='1'>"
output += "<tr>"
output += "<td>Game ID</td><td># Players</td>"
output += "</tr>"
if server_list != None:
for game in server_list:
if not server_list[game]:
continue
output += "<tr>"
output += "<td>" + game + "</td>"
output += "<td><center>%d</center></td>" % (len(server_list[game]))
output += "</tr>"
output += "</table>"
output += "<br>"
output += "<i>Last updated: %s</i><br>" % (self.stats.get_last_update_time())
output += "</html>"
output = self.header
if server_list is not None:
output += "".join(self.row % (game, len(server_list[game]))
for game in server_list
if not server_list[game])
output += self.footer % (self.stats.get_last_update_time())
return output
class InternalStatsServer(object):
"""Internal Statistics server.
Running on port 9001 by default: http://127.0.0.1:9001/
Can be displayed in json format: http://127.0.0.1:9001/json
"""
def __init__(self):
self.last_update = 0
self.next_update = 0
self.server_list = None
self.seconds_per_update = 60 # The number of seconds to wait before updating the server list
# The number of seconds to wait before updating the server list
self.seconds_per_update = 60
def start(self):
manager_address = ("127.0.0.1", 27500)
manager_address = dwc_config.get_ip_port('GameSpyManager')
manager_password = ""
self.server_manager = GameSpyServerDatabase(address = manager_address, authkey= manager_password)
self.server_manager = GameSpyServerDatabase(address=manager_address,
authkey=manager_password)
self.server_manager.connect()
site = server.Site(StatsPage(self))
reactor.listenTCP(9001, site)
reactor.listenTCP(dwc_config.get_port('InternalStatsServer'), site)
try:
if reactor.running == False:
if not reactor.running:
reactor.run(installSignalHandlers=0)
except ReactorAlreadyRunning:
pass
def get_server_list(self, force_update = False):
if force_update == True or self.next_update == 0 or self.next_update - time.time() <= 0:
def get_server_list(self, force_update=False):
if force_update or self.next_update == 0 or \
self.next_update - time.time() <= 0:
self.last_update = time.time()
self.next_update = time.time() + self.seconds_per_update
self.server_list = self.server_manager.get_server_list()._getvalue()
self.server_list = self.server_manager.get_server_list() \
._getvalue()
logger.log(logging.DEBUG, self.server_list)
logger.log(logging.DEBUG, "%s", self.server_list)
return self.server_list
def get_last_update_time(self):
return str(datetime.datetime.fromtimestamp(self.last_update))
if __name__ == "__main__":
stats = InternalStatsServer()
stats.start()

View File

@@ -1,21 +1,24 @@
# DWC Network Server Emulator
# Copyright (C) 2014 polaris-
# Copyright (C) 2014 ToadKing
# Copyright (C) 2014 AdmiralCurtiss
# Copyright (C) 2014 msoucy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""DWC Network Server Emulator
Copyright (C) 2014 polaris-
Copyright (C) 2014 ToadKing
Copyright (C) 2014 AdmiralCurtiss
Copyright (C) 2014 msoucy
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from gamespy_player_search_server import GameSpyPlayerSearchServer
from gamespy_profile_server import GameSpyProfileServer
@@ -29,6 +32,7 @@ from internal_stats_server import InternalStatsServer
from admin_page_server import AdminPageServer
from storage_server import StorageServer
from gamestats_server_http import GameStatsServer
from register_page import RegPageServer
import gamespy.gs_database as gs_database
@@ -36,24 +40,28 @@ import threading
if __name__ == "__main__":
# Let database initialize before starting any servers.
# This fixes any conflicts where two servers find an uninitialized database at the same time and both try to
# initialize it.
"""Let database initialize before starting any servers.
This fixes any conflicts where two servers find an uninitialized database
at the same time and both try to initialize it.
"""
db = gs_database.GamespyDatabase()
db.initialize_database()
db.close()
servers = [
GameSpyBackendServer,
GameSpyQRServer,
GameSpyProfileServer,
GameSpyPlayerSearchServer,
GameSpyGamestatsServer,
#GameSpyServerBrowserServer,
# GameSpyServerBrowserServer,
GameSpyNatNegServer,
NasServer,
InternalStatsServer,
AdminPageServer,
RegPageServer,
StorageServer,
GameStatsServer,
]

View File

@@ -1,24 +1,26 @@
# DWC Network Server Emulator
# Copyright (C) 2014 polaris-
# Copyright (C) 2014 ToadKing
# Copyright (C) 2014 AdmiralCurtiss
# Copyright (C) 2014 msoucy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""DWC Network Server Emulator
Copyright (C) 2014 polaris-
Copyright (C) 2014 ToadKing
Copyright (C) 2014 AdmiralCurtiss
Copyright (C) 2014 msoucy
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import base64
import json
import logging
import time
import urlparse
@@ -30,21 +32,40 @@ import random
import traceback
import gamespy.gs_database as gs_database
import gamespy.gs_utility as gs_utils
import other.utils as utils
import dwc_config
logger_output_to_console = True
logger_output_to_file = True
logger_name = "NasServer"
logger_filename = "nas_server.log"
logger = utils.create_logger(logger_name, logger_filename, -1, logger_output_to_console, logger_output_to_file)
logger = dwc_config.get_logger('NasServer')
# if a game from this list requests a file listing, the server will return that only one exists and return a random one
# this is used for Mystery Gift distribution on Generation 4 Pokemon games
gamecodes_return_random_file = ['ADAD', 'ADAE', 'ADAF', 'ADAI', 'ADAJ', 'ADAK', 'ADAS', 'CPUD', 'CPUE', 'CPUF', 'CPUI', 'CPUJ', 'CPUK', 'CPUS', 'IPGD', 'IPGE', 'IPGF', 'IPGI', 'IPGJ', 'IPGK', 'IPGS']
# If a game from this list requests a file listing, the server will return
# that only one exists and return a random one.
# This is used for Mystery Gift distribution on Generation 4 Pokemon games
gamecodes_return_random_file = [
'ADAD',
'ADAE',
'ADAF',
'ADAI',
'ADAJ',
'ADAK',
'ADAS',
'CPUD',
'CPUE',
'CPUF',
'CPUI',
'CPUJ',
'CPUK',
'CPUS',
'IPGD',
'IPGE',
'IPGF',
'IPGI',
'IPGJ',
'IPGK',
'IPGS'
]
address = dwc_config.get_ip_port('NasServer')
#address = ("0.0.0.0", 80)
address = ("127.0.0.1", 9000)
class NasServer(object):
def start(self):
@@ -52,20 +73,24 @@ class NasServer(object):
t = threading.Thread(target=httpd.serve_forever)
t.daemon = True
t.start()
logger.log(logging.INFO, "Now listening for connections on %s:%d...", address[0], address[1])
logger.log(logging.INFO, "Now listening for connections on %s:%d...",
address[0], address[1])
httpd.serve_forever()
class NasHTTPServer(SocketServer.ThreadingMixIn,BaseHTTPServer.HTTPServer):
class NasHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
def __init__(self, server_address, RequestHandlerClass):
#self.db = gs_database.GamespyDatabase()
BaseHTTPServer.HTTPServer.__init__(self, server_address, RequestHandlerClass)
# self.db = gs_database.GamespyDatabase()
BaseHTTPServer.HTTPServer.__init__(self, server_address,
RequestHandlerClass)
class NasHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def version_string(self):
return "Nintendo Wii (http)"
def do_GET(self):
self.server = lambda:None
self.server = lambda: None
self.server.db = gs_database.GamespyDatabase()
try:
@@ -77,25 +102,31 @@ class NasHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write("ok")
except:
logger.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
logger.log(logging.ERROR, "Unknown exception: %s",
traceback.format_exc())
def do_POST(self):
self.server = lambda:None
self.server = lambda: None
self.server.db = gs_database.GamespyDatabase()
try:
length = int(self.headers['content-length'])
post = self.str_to_dict(self.rfile.read(length))
if self.client_address[0] == '127.0.0.1':
client_address = (self.headers.get('x-forwarded-for', self.client_address[0]), self.client_address[1])
client_address = (
self.headers.get('x-forwarded-for',
self.client_address[0]),
self.client_address[1]
)
else:
client_address = self.client_address
post['ipaddr'] = client_address[0]
if self.path == "/ac":
logger.log(logging.DEBUG, "Request to %s from %s", self.path, client_address)
logger.log(logging.DEBUG, post)
logger.log(logging.DEBUG, "Request to %s from %s",
self.path, client_address)
logger.log(logging.DEBUG, "%s", post)
ret = {
"datetime": time.strftime("%Y%m%d%H%M%S"),
"retry": "0"
@@ -108,7 +139,9 @@ class NasHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
if action == "acctcreate":
# TODO: test for duplicate accounts
if self.server.db.is_banned(post):
logger.log(logging.DEBUG, "acctcreate denied for banned user "+str(post))
logger.log(logging.DEBUG,
"acctcreate denied for banned user %s",
str(post))
ret = {
"datetime": time.strftime("%Y%m%d%H%M%S"),
"returncd": "3913",
@@ -118,16 +151,21 @@ class NasHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
}
else:
ret["returncd"] = "002"
ret['userid'] = self.server.db.get_next_available_userid()
ret['userid'] = \
self.server.db.get_next_available_userid()
logger.log(logging.DEBUG, "acctcreate response to %s", client_address)
logger.log(logging.DEBUG, ret)
logger.log(logging.DEBUG,
"acctcreate response to %s",
client_address)
logger.log(logging.DEBUG, "%s", ret)
ret = self.dict_to_str(ret)
elif action == "login":
if self.server.db.is_banned(post):
logger.log(logging.DEBUG, "login denied for banned user "+str(post))
logger.log(logging.DEBUG,
"login denied for banned user %s",
str(post))
ret = {
"datetime": time.strftime("%Y%m%d%H%M%S"),
"returncd": "3914",
@@ -135,28 +173,36 @@ class NasHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
"retry": "1",
"reason": "User banned."
}
#Un-comment these lines to enable console registration feature
#elif not self.server.db.pending(post):
#logger.log(logging.DEBUG, "Login denied - Unknown console"+str(post))
#ret = {
#"datetime": time.strftime("%Y%m%d%H%M%S"),
#"returncd": "3921",
#"locator": "gamespy.com",
#"retry": "1",
#}
#elif not self.server.db.registered(post):
#logger.log(logging.DEBUG, "Login denied - console pending"+str(post))
#ret = {
#"datetime": time.strftime("%Y%m%d%H%M%S"),
#"returncd": "3888",
#"locator": "gamespy.com",
#"retry": "1",
#}
# Un-comment these lines to enable console registration
# feature
# elif not self.server.db.pending(post):
# logger.log(logging.DEBUG,
# "Login denied - Unknown console %s",
# post)
# ret = {
# "datetime": time.strftime("%Y%m%d%H%M%S"),
# "returncd": "3921",
# "locator": "gamespy.com",
# "retry": "1",
# }
# elif not self.server.db.registered(post):
# logger.log(logging.DEBUG,
# "Login denied - console pending %s",
# post)
# ret = {
# "datetime": time.strftime("%Y%m%d%H%M%S"),
# "returncd": "3888",
# "locator": "gamespy.com",
# "retry": "1",
# }
else:
challenge = utils.generate_random_str(8)
post["challenge"] = challenge
authtoken = self.server.db.generate_authtoken(post["userid"], post)
authtoken = self.server.db.generate_authtoken(
post["userid"],
post
)
ret.update({
"returncd": "001",
"locator": "gamespy.com",
@@ -164,42 +210,60 @@ class NasHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
"token": authtoken,
})
logger.log(logging.DEBUG, "login response to %s", client_address)
logger.log(logging.DEBUG, ret)
logger.log(logging.DEBUG, "login response to %s",
client_address)
logger.log(logging.DEBUG, "%s", ret)
ret = self.dict_to_str(ret)
elif action == "SVCLOC" or action == "svcloc": # Get service based on service id number
elif action == "SVCLOC" or action == "svcloc":
# Get service based on service id number
ret["returncd"] = "007"
ret["statusdata"] = "Y"
authtoken = self.server.db.generate_authtoken(post["userid"], post)
authtoken = self.server.db.generate_authtoken(
post["userid"],
post
)
if 'svc' in post:
if post["svc"] in ("9000", "9001"): # DLC host = 9000
ret["svchost"] = self.headers['host'] # in case the client's DNS isn't redirecting dls1.nintendowifi.net
if post["svc"] in ("9000", "9001"):
# DLC host = 9000
# In case the client's DNS isn't redirecting to
# dls1.nintendowifi.net
ret["svchost"] = self.headers['host']
# Brawl has 2 host headers which Apache chokes on, so only return the first one or else it won't work
# Brawl has 2 host headers which Apache chokes
# on, so only return the first one or else it
# won't work
ret["svchost"] = ret["svchost"].split(',')[0]
if post["svc"] == 9000:
ret["token"] = authtoken
else:
ret["servicetoken"] = authtoken
elif post["svc"] == "0000": # Pokemon requests this for some things
elif post["svc"] == "0000":
# Pokemon requests this for some things
ret["servicetoken"] = authtoken
ret["svchost"] = "n/a"
else:
# Empty svc - Fix Error Code 24101 (Boom Street)
ret["svchost"] = "n/a"
ret["servicetoken"] = authtoken
logger.log(logging.DEBUG, "svcloc response to %s", client_address)
logger.log(logging.DEBUG, ret)
logger.log(logging.DEBUG, "svcloc response to %s",
client_address)
logger.log(logging.DEBUG, "%s", ret)
ret = self.dict_to_str(ret)
else:
logger.log(logging.WARNING, "Unknown action request %s from %s!", self.path, client_address)
logger.log(logging.WARNING,
"Unknown action request %s from %s!",
self.path, client_address)
elif self.path == "/pr":
logger.log(logging.DEBUG, "Request to %s from %s", self.path, client_address)
logger.log(logging.DEBUG, post)
logger.log(logging.DEBUG, "Request to %s from %s",
self.path, client_address)
logger.log(logging.DEBUG, "%s", post)
words = len(post["words"].split('\t'))
wordsret = "0" * words
ret = {
@@ -209,20 +273,21 @@ class NasHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
}
for l in "ACEJKP":
ret["prwords"+l] = wordsret
ret["prwords" + l] = wordsret
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.send_header("NODE", "wifiappe1")
logger.log(logging.DEBUG, "pr response to %s", client_address)
logger.log(logging.DEBUG, ret)
logger.log(logging.DEBUG, "%s", ret)
ret = self.dict_to_str(ret)
elif self.path == "/download":
logger.log(logging.DEBUG, "Request to %s from %s", self.path, client_address)
logger.log(logging.DEBUG, post)
logger.log(logging.DEBUG, "Request to %s from %s",
self.path, client_address)
logger.log(logging.DEBUG, "%s", post)
action = post["action"]
@@ -232,16 +297,18 @@ class NasHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
dlc_contenttype = False
if os.path.commonprefix([dlcdir, dlcpath]) != dlcdir:
logging.log(logging.WARNING, 'Attempted directory traversal attack "%s", cancelling.', dlcpath)
logging.log(logging.WARNING,
'Attempted directory traversal attack "%s",'
' cancelling.', dlcpath)
self.send_response(403)
return
def safeloadfi(fn, mode='rb'):
'''
safeloadfi : string -> string
"""safeloadfi : string -> string
Safely load contents of a file, given a filename, and closing the file afterward
'''
Safely load contents of a file, given a filename,
and closing the file afterward.
"""
with open(os.path.join(dlcpath, fn), mode) as fi:
return fi.read()
@@ -260,7 +327,8 @@ class NasHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
attr3 = post.get("attr3", None)
dlcfi = safeloadfi("_list.txt")
lst = self.filter_list(dlcfi, attr1, attr2, attr3)
lst = self.filter_list(dlcfi,
attr1, attr2, attr3)
count = self.get_file_count(lst)
ret = "%d" % count
@@ -269,10 +337,10 @@ class NasHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
num = post.get("num", None)
offset = post.get("offset", None)
if num != None:
if num is not None:
num = int(num)
if offset != None:
if offset is not None:
offset = int(offset)
attr1 = post.get("attr1", None)
@@ -281,76 +349,123 @@ class NasHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
if os.path.exists(dlcpath):
# Look for a list file first.
# If the list file exists, send the entire thing back to the client.
# If the list file exists, send the entire thing back
# to the client.
if os.path.isfile(os.path.join(dlcpath, "_list.txt")):
if post["gamecd"].startswith("IRA") and attr1.startswith("MYSTERY"):
# Pokemon BW Mystery Gifts, until we have a better solution for that
ret = self.filter_list(safeloadfi("_list.txt"), attr1, attr2, attr3)
ret = self.filter_list_g5_mystery_gift(ret, post["rhgamecd"])
ret = self.filter_list_by_date(ret, post["token"])
elif post["gamecd"] in gamecodes_return_random_file:
if post["gamecd"].startswith("IRA") and \
attr1.startswith("MYSTERY"):
# Pokemon BW Mystery Gifts, until we have a
# better solution for that
ret = self.filter_list(
safeloadfi("_list.txt"),
attr1, attr2, attr3
)
ret = self.filter_list_g5_mystery_gift(
ret,
post["rhgamecd"]
)
ret = self.filter_list_by_date(
ret,
post["token"]
)
elif post["gamecd"] in \
gamecodes_return_random_file:
# Pokemon Gen 4 Mystery Gifts, same here
ret = self.filter_list(safeloadfi("_list.txt"), attr1, attr2, attr3)
ret = self.filter_list_by_date(ret, post["token"])
ret = self.filter_list(
safeloadfi("_list.txt"),
attr1, attr2, attr3
)
ret = self.filter_list_by_date(
ret,
post["token"]
)
else:
# default case for most games
ret = self.filter_list(safeloadfi("_list.txt"), attr1, attr2, attr3, num, offset)
ret = self.filter_list(
safeloadfi("_list.txt"),
attr1, attr2, attr3,
num, offset
)
if action == "contents":
# Get only the base filename just in case there is a path involved somewhere in the filename string.
# Get only the base filename just in case there is a path
# involved somewhere in the filename string.
dlc_contenttype = True
contents = os.path.basename(post["contents"])
ret = safeloadfi(contents)
self.send_response(200)
if dlc_contenttype == True:
if dlc_contenttype is True:
self.send_header("Content-type", "application/x-dsdl")
self.send_header("Content-Disposition", "attachment; filename=\"" + post["contents"] + "\"")
self.send_header("Content-Disposition",
"attachment; filename=\"" +
post["contents"] + "\"")
else:
self.send_header("Content-type", "text/plain")
self.send_header("X-DLS-Host", "http://127.0.0.1/")
logger.log(logging.DEBUG, "download response to %s", client_address)
logger.log(logging.DEBUG, "download response to %s",
client_address)
#if dlc_contenttype == False:
# logger.log(logging.DEBUG, ret)
# if dlc_contenttype is False:
# logger.log(logging.DEBUG, "%s", ret)
else:
self.send_response(404)
logger.log(logging.WARNING, "Unknown path request %s from %s!", self.path, client_address)
logger.log(logging.WARNING,
"Unknown path request %s from %s!",
self.path, client_address)
return
self.send_header("Content-Length", str(len(ret)))
self.end_headers()
self.wfile.write(ret)
except:
logger.log(logging.ERROR, "Unknown exception: %s" % traceback.format_exc())
logger.log(logging.ERROR, "Unknown exception: %s",
traceback.format_exc())
def str_to_dict(self, s):
ret = urlparse.parse_qs(s)
# Enable keep_blank_values, skipped otherwise.
# TODO: Move this in utils module?
ret = urlparse.parse_qs(s, True)
for k, v in ret.iteritems():
try:
# I'm not sure about the replacement for '-', but it'll at least let it be decoded.
# For the most part it's not important since it's mostly used for the devname/ingamesn fields.
ret[k] = base64.b64decode( urlparse.unquote( v[0] ).replace("*", "=").replace("?", "/").replace(">","+").replace("-","/") )
# I'm not sure about the replacement for '-', but it'll at
# least let it be decoded.
# For the most part it's not important since it's mostly
# used for the devname/ingamesn fields.
ret[k] = base64.b64decode(urlparse.unquote(v[0])
.replace("*", "=")
.replace("?", "/")
.replace(">", "+")
.replace("-", "/"))
except TypeError:
print "Could not decode following string: ret[%s] = %s" % (k, v[0])
print "Could not decode following string: ret[%s] = %s" \
% (k, v[0])
print "url: %s" % s
ret[k] = v[0] # If you don't assign it like this it'll be a list, which breaks other code.
# If you don't assign it like this it'll be a list, which
# breaks other code.
ret[k] = v[0]
return ret
def dict_to_str(self, dict):
"""Convert dict to str.
nas(wii).nintendowifi.net has a URL query-like format but does not
use encoding for special characters.
"""
for k, v in dict.iteritems():
dict[k] = base64.b64encode(v).replace("=", "*")
# nas(wii).nintendowifi.net has a URL query-like format but does not use encoding for special characters
return "&".join("{!s}={!s}".format(k, v) for k, v in dict.items()) + "\r\n"
# custom selection for generation 5 mystery gifts, so that the random or data-based selection still works properly
return "&".join("{!s}={!s}".format(k, v) for k, v in dict.items()) + \
"\r\n"
def filter_list_g5_mystery_gift(self, data, rhgamecd):
"""Custom selection for generation 5 mystery gifts, so that the random
or data-based selection still works properly."""
if rhgamecd[2] == 'A':
filterBit = 0x100000
elif rhgamecd[2] == 'B':
@@ -362,7 +477,7 @@ class NasHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
else:
# unknown game, can't filter
return data
output = []
for line in data.splitlines():
lineBits = int(line.split('\t')[3], 16)
@@ -371,8 +486,9 @@ class NasHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
return '\r\n'.join(output) + '\r\n'
def filter_list_by_date(self, data, token):
# allow user to control which file to receive by setting the local date
# selected file will be the one at index (day of year) mod (file count)
"""Allow user to control which file to receive by setting
the local date selected file will be the one at
index (day of year) mod (file count)."""
try:
userData = self.server.db.get_nas_login(token)
date = time.strptime(userData['devtime'], '%y%m%d%H%M%S')
@@ -381,34 +497,42 @@ class NasHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
except:
ret = self.filter_list_random_files(data, 1)
return ret
def filter_list_random_files(self, data, count):
# Get [count] random files from the filelist
"""Get [count] random files from the filelist."""
samples = random.sample(data.splitlines(), count)
return '\r\n'.join(samples) + '\r\n'
def filter_list(self, data, attr1 = None, attr2 = None, attr3 = None, num = None, offset = None):
if attr1 == None and attr2 == None and attr3 == None and num == None and offset == None:
def filter_list(self, data, attr1=None, attr2=None, attr3=None,
num=None, offset=None):
"""Filter the list based on the attribute fields.
If nothing matches, at least return a newline.
Pokemon BW at least expects this and will error without it.
"""
if attr1 is None and attr2 is None and attr3 is None and \
num is None and offset is None:
# Nothing to filter, just return the input data
return data
# Filter the list based on the attribute fields
nc = lambda a, b: (a is None or a == b)
attrs = lambda data: (len(data) == 6 and nc(attr1, data[2]) and nc(attr2, data[3]) and nc(attr3, data[4]))
output = filter(lambda line: attrs(line.split("\t")), data.splitlines())
attrs = lambda data: (len(data) == 6 and nc(attr1, data[2]) and
nc(attr2, data[3]) and nc(attr3, data[4]))
output = filter(lambda line: attrs(line.split("\t")),
data.splitlines())
if offset != None:
if offset is not None:
output = output[offset:]
if num != None:
if num is not None:
output = output[:num]
# if nothing matches, at least return a newline; Pokemon BW at least expects this and will error without it
return '\r\n'.join(output) + '\r\n'
def get_file_count(self, data):
return sum(1 for line in data.splitlines() if line)
if __name__ == "__main__":
nas = NasServer()
nas.start()

View File

@@ -1,19 +1,22 @@
# DWC Network Server Emulator
# Copyright (C) 2014 polaris-
# Copyright (C) 2014 msoucy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""DWC Network Server Emulator
Copyright (C) 2014 polaris-
Copyright (C) 2014 msoucy
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging
import logging.handlers
@@ -23,42 +26,51 @@ import struct
import ctypes
import os
def generate_random_str_from_set(ln, chs):
"""Generate a random string of size <ln> based on charset <chs>."""
return ''.join(random.choice(chs) for _ in range(ln))
def generate_random_str(ln, chs=""):
return generate_random_str_from_set(ln, chs or (string.ascii_letters + string.digits))
"""Generate a random string of size <ln>."""
return generate_random_str_from_set(
ln,
chs or (string.ascii_letters + string.digits)
)
def generate_random_number_str(ln):
"""Generate a random number string of size <ln>."""
return generate_random_str_from_set(ln, string.digits)
def generate_random_hex_str(ln):
"""Generate a random hexadecimal number string of size <ln>."""
return generate_random_str_from_set(ln, string.hexdigits.lower())
# Code: Tetris DS @ 020573F4
def calculate_crc8(inp):
"""
Code: Tetris DS @ 020573F4
"""
crc_table = [
0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3
0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3
]
crc = 0
@@ -69,33 +81,44 @@ def calculate_crc8(inp):
def base32_encode(num, reverse=True):
"""Encode a number in base 32.
Result string is reversed by default.
"""
alpha = "0123456789abcdefghijklmnopqrstuv"
encoded = ""
while num > 0:
encoded += alpha[num & 0x1f]
num = num >> 5
num >>= 5
while len(encoded) < 9:
encoded += "0"
encoded.ljust(9, '0')
if reverse == True:
encoded = encoded[::-1] # Reverse string
if reverse:
encoded = encoded[::-1]
return encoded
def base32_decode(s, reverse=False):
"""Decode a number in base 32.
Input string is not reversed by default.
"""
alpha = "0123456789abcdefghijklmnopqrstuv"
if reverse == True:
s = s[::-1] # Reverse string
if reverse:
s = s[::-1]
return reduce(lambda orig, b: ((orig<<5)|alpha.index(b)), s, 0)
return reduce(lambda orig, b: ((orig << 5) | alpha.index(b)), s, 0)
# Number routines
def get_num_from_bytes(data, idx, fmt, bigEndian=False):
"""Get number from bytes.
Endianness by default is little.
"""
return struct.unpack_from("<>"[bigEndian] + fmt, buffer(bytearray(data)), idx)[0]
# Instead of passing slices, pass the buffer and index so we can calculate
@@ -103,59 +126,100 @@ def get_num_from_bytes(data, idx, fmt, bigEndian=False):
def get_short_signed(data, idx, be=False):
"""Get short from bytes.
Endianness by default is little.
"""
return get_num_from_bytes(data, idx, 'h', be)
def get_short(data, idx, be=False):
"""Get unsigned short from bytes.
Endianness by default is little.
"""
return get_num_from_bytes(data, idx, 'H', be)
def get_int_signed(data, idx, be=False):
"""Get int from bytes.
Endianness by default is little.
"""
return get_num_from_bytes(data, idx, 'i', be)
def get_int(data, idx, be=False):
"""Get unsigned int from bytes.
Endianness by default is little.
"""
return get_num_from_bytes(data, idx, 'I', be)
def get_ip(data, idx, be=False):
"""Get IP from bytes.
Endianness by default is little.
"""
return ctypes.c_int32(get_int(data, idx, be)).value
def get_string(data, idx):
"""Get string from bytes."""
data = data[idx:]
end = data.index('\x00')
return str(''.join(data[:end]))
def get_bytes_from_num(num, fmt, bigEndian=False):
"""Get bytes from number.
Endianness by default is little.
"""
return struct.pack("<>"[bigEndian] + fmt, num)
def get_bytes_from_short_signed(num, be=False):
"""Get bytes from short.
Endianness by default is little.
"""
return get_bytes_from_num(num, 'h', be)
def get_bytes_from_short(num, be=False):
"""Get bytes from unsigned short.
Endianness by default is little.
"""
return get_bytes_from_num(num, 'H', be)
def get_bytes_from_int_signed(num, be=False):
"""Get bytes from int.
Endianness by default is little.
"""
return get_bytes_from_num(num, 'i', be)
def get_bytes_from_int(num, be=False):
"""Get bytes from unsigned int.
Endianness by default is little.
"""
return get_bytes_from_num(num, 'I', be)
# For server logging
def create_logger(loggername, filename, level, log_to_console, log_to_file):
"""Server logging."""
log_folder = "logs"
# Create log folder if it doesn't exist
if not os.path.exists(log_folder):
os.makedirs(log_folder)
# Build full path to log file
filename = os.path.join(log_folder, filename)
@@ -164,55 +228,100 @@ def create_logger(loggername, filename, level, log_to_console, log_to_file):
fmt = "[%(asctime)s | " + loggername + "] %(message)s"
date_format = "%Y-%m-%d %H:%M:%S"
#logging.basicConfig(format=format, datefmt=date_format)
# logging.basicConfig(format=format, datefmt=date_format)
logger = logging.getLogger(loggername)
logger.setLevel(level)
# Only needed when logging.basicConfig isn't set.
if log_to_console == True:
if log_to_console:
console_logger = logging.StreamHandler()
console_logger.setFormatter(
logging.Formatter(fmt, datefmt=date_format))
logging.Formatter(fmt, datefmt=date_format)
)
logger.addHandler(console_logger)
if log_to_file == True and filename != "":
# Use a rotating log set to rotate every night at midnight with a max of 10 backups
file_logger = logging.handlers.TimedRotatingFileHandler(filename, when='midnight', backupCount=10) #logging.FileHandler(filename)
file_logger.setFormatter(logging.Formatter(fmt, datefmt=date_format))
if log_to_file and filename:
# Use a rotating log set to rotate every night at midnight with a
# max of 10 backups
file_logger = logging.handlers.TimedRotatingFileHandler(
filename,
when='midnight',
backupCount=10) # logging.FileHandler(filename)
file_logger.setFormatter(
logging.Formatter(fmt, datefmt=date_format)
)
logger.addHandler(file_logger)
return logger
def print_hex(data, cols=16):
print pretty_print_hex(data, cols)
def print_hex(data, cols=16, sep=' ', pretty=True):
"""Print data in hexadecimal.
Customizable separator and columns number.
Can be pretty printed but takes more time.
"""
if pretty:
print pretty_print_hex(data, cols, sep)
else:
print sep.join("%02x" % b for b in bytearray(data))
def pretty_print_hex(orig_data, cols=16):
def pretty_print_hex(orig_data, cols=16, sep=' '):
"""Hexadecimal pretty print.
Takes ~1s per characters.
Customizable separator and columns number.
"""
data = bytearray(orig_data)
output = "\n"
end = len(data)
line = "%08x | %-*s | %s\n"
size = cols * 3 - 1
for i in range(len(data) / cols + 1):
output += "%08x | " % (i * 16)
c = 0
for x in range(cols):
if (i * cols + x + 1) > len(data):
break
output += "%02x " % data[i * cols + x]
c += 1
c = cols - c
output += " " * (c * 3 + 1)
for x in range(cols):
if (i * cols + x + 1) > len(data):
break
if data[i * cols + x] < 0x21 or data[i * cols + x] >= 0x7f:
output += "."
else:
output += "%c" % data[i * cols + x]
output += "\n"
i = 0
output = ""
while i < end:
j = i + cols if i + cols <= end \
else end - i
output += line % (
i,
size,
sep.join("%02x" % c for c in data[i:j]),
"".join(chr(c) if chr(c) in string.printable else
'.'
for c in data[i:j])
)
i += cols
return output
# def pretty_print_hex(orig_data, cols=16):
# """Takes ~1.5s per characters"""
#
# data = bytearray(orig_data)
# output = "\n"
#
# for i in range(len(data) / cols + 1):
# output += "%08x | " % (i * 16)
#
# c = 0
# for x in range(cols):
# if (i * cols + x + 1) > len(data):
# break
#
# output += "%02x " % data[i * cols + x]
# c += 1
#
# c = cols - c
# output += " " * (c * 3 + 1)
# for x in range(cols):
# if (i * cols + x + 1) > len(data):
# break
#
# if not chr(data[i * cols + x]) in string.printable:
# output += "."
# else:
# output += "%c" % data[i * cols + x]
# output += "\n"
#
# return output

View File

@@ -1,62 +1,69 @@
# DWC Network Server Emulator
# Copyright (C) 2014 SMTDDR
# Copyright (C) 2014 kyle95wm
# Copyright (C) 2014 AdmiralCurtiss
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""DWC Network Server Emulator
Copyright (C) 2014 SMTDDR
Copyright (C) 2014 kyle95wm
Copyright (C) 2014 AdmiralCurtiss
Copyright (C) 2015 Sepalani
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from twisted.web import server, resource
from twisted.internet import reactor
from twisted.internet.error import ReactorAlreadyRunning
import re
import base64
import codecs
import codecs
import sqlite3
import collections
import json
import time
import datetime
import logging
import other.utils as utils
import gamespy
import gamespy.gs_utility as gs_utils
import dwc_config
logger = dwc_config.get_logger('RegisterPage')
_, port = dwc_config.get_ip_port('RegisterPage')
class RegPage(resource.Resource):
isLeaf = True
def __init__(self,regpage):
def __init__(self, regpage):
self.regpage = regpage
def get_header(self, title = None):
def get_header(self, title=None):
if not title:
title = 'Register a Console'
s = (
'<html>'
'<head>'
'<title>' + title + '</title>'
'</head>'
'<body>'
'<p>'
'<b>Register a console</b>'
'</p>'
)
s = """
<html>
<head>
<title>%s</title>
</head>
<body>
<p>
<b>Register a console</b>
</p>""" % title
return s
def get_footer(self):
s = (
'</body>'
'</html>'
)
s = """
</body>
</html>"""
return s
def update_maclist(self, request):
@@ -65,13 +72,21 @@ class RegPage(resource.Resource):
macadr = request.args['macadr'][0].strip()
actiontype = request.args['action'][0]
macadr = macadr.lower()
if not re.match("[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", macadr):
if not re.match("[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$",
macadr):
request.setResponseCode(500)
return "The MAC you entered was invalid. Please click the back button and try again!"
macadr = macadr.replace(":","").replace("-","")
return "The MAC you entered was invalid." \
"Please click the back button and try again!"
macadr = macadr.replace(":", "").replace("-", "")
if actiontype == 'add':
dbconn.cursor().execute('insert into pending values(?)',(macadr,))
responsedata = "Added %s to pending list. Please close this window now. It's also not a bad idea to check back on the status of your activation by attempting to connect your console to the server." % (macadr)
dbconn.cursor().execute(
'INSERT INTO pending VALUES(?)',
(macadr,)
)
responsedata = "Added %s to pending list." % (macadr)
responsedata += "Please close this window now."
" It's also not a bad idea to check back on the status of your"
" activation by attempting to connect your console to the server."
dbconn.commit()
dbconn.close()
request.setHeader("Content-Type", "text/html; charset=utf-8")
@@ -87,13 +102,14 @@ class RegPage(resource.Resource):
def render_maclist(self, request):
address = request.getClientIP()
dbconn = sqlite3.connect('gpcm.db')
responsedata = (""
"<form action='updatemaclist' method='POST'>"
"macadr (must be in the format of aa:bb:cc:dd:ee:ff or aa-bb-cc-dd-ee-ff):<input type='text' name='macadr'>\r\n"
"<input type='hidden' name='action' value='add'>\r\n"
"<input type='submit' value='Register console'></form>\r\n"
"<table border='1'>"
"")
responsedata = """
<form action='updatemaclist' method='POST'>
macadr (must be in the format of %s or %s):
<input type='text' name='macadr'>
<input type='hidden' name='action' value='add'>
<input type='submit' value='Register console'>
</form>
<table border='1'>""" % ('aa:bb:cc:dd:ee:ff', 'aa-bb-cc-dd-ee-ff')
dbconn.close()
request.setHeader("Content-Type", "text/html; charset=utf-8")
return responsedata
@@ -104,7 +120,7 @@ class RegPage(resource.Resource):
if request.path == "/register":
title = 'Register a Console'
response = self.render_maclist(request)
return self.get_header(title) + response + self.get_footer()
def render_POST(self, request):
@@ -113,16 +129,20 @@ class RegPage(resource.Resource):
else:
return self.get_header() + self.get_footer()
port = 9998
class RegPageServer(object):
def start(self):
site = server.Site(RegPage(self))
reactor.listenTCP(port, site)
logger.log(logging.INFO,
"Now listening for connections on port %d...",
port)
try:
if reactor.running == False:
if not reactor.running:
reactor.run(installSignalHandlers=0)
except ReactorAlreadyRunning:
pass
if __name__ == "__main__":
RegPageServer().start()

View File

@@ -1,20 +1,22 @@
# DWC Network Server Emulator
# Copyright (C) 2014 polaris-
# Copyright (C) 2014 AdmiralCurtiss
# Copyright (C) 2014 msoucy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""DWC Network Server Emulator
Copyright (C) 2014 polaris-
Copyright (C) 2014 AdmiralCurtiss
Copyright (C) 2014 msoucy
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import os
import random
@@ -27,15 +29,12 @@ import xml.dom.minidom as minidom
import other.utils as utils
import gamespy.gs_database as gs_database
logger_output_to_console = True
logger_output_to_file = True
logger_name = "StorageServer"
logger_filename = "storage_server.log"
logger = utils.create_logger(logger_name, logger_filename, -1, logger_output_to_console, logger_output_to_file)
import dwc_config
# Paths to ProxyPass: /SakeStorageServer, /SakeFileServer
address = ("127.0.0.1", 8000)
logger = dwc_config.get_logger('StorageServer')
address = dwc_config.get_ip_port('StorageServer')
def escape_xml(s):
s = s.replace( "&", "&amp;" )
@@ -45,12 +44,14 @@ def escape_xml(s):
s = s.replace( ">", "&gt;" )
return s
class StorageServer(object):
def start(self):
httpd = StorageHTTPServer((address[0], address[1]), StorageHTTPServerHandler)
logger.log(logging.INFO, "Now listening for connections on %s:%d...", address[0], address[1])
httpd.serve_forever()
class StorageHTTPServer(BaseHTTPServer.HTTPServer):
def __init__(self, server_address, RequestHandlerClass):
BaseHTTPServer.HTTPServer.__init__(self, server_address, RequestHandlerClass)
@@ -207,10 +208,14 @@ class StorageHTTPServer(BaseHTTPServer.HTTPServer):
except TypeError:
return 'UNKNOWN'
class IllegalColumnAccessException(Exception):
pass
class FilterSyntaxException(Exception):
pass
class StorageHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def confirm_columns(self, columndata, table):

View File

@@ -1,8 +1,10 @@
# Import the profile and friend data collected from the GameSpy servers by Wiimm
# Put all *-nick and *-fc files to be imported into t into the data folder and then run this program to import them
# into the database.
#
# This may take some time to import all of the data on bigger files if you check if each entry is already in the database.
"""Import the profile and friend data collected from the GameSpy servers by Wiimm
Put all *-nick and *-fc files to be imported into t into the data folder and then run this program to import them
into the database.
This may take some time to import all of the data on bigger files if you check if each entry is already in the database.
"""
import glob
import sys
@@ -45,7 +47,7 @@ for nickfile in glob.glob("data/*-nick"):
uniquenick = s[1]
# Uncomment to check if the user exists before inserting, but it slows down things greatly.
#if db.check_profile_exists(profileid) != None:
#if db.check_profile_exists(profileid) is not None:
# pass
firstname = s[2]