123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687 |
- """DWC Network Server Emulator
- Copyright (C) 2014 polaris-
- Copyright (C) 2014 AdmiralCurtiss
- Copyright (C) 2014 msoucy
- Copyright (C) 2018 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 os
- import random
- import logging
- import BaseHTTPServer
- import cgi
- import urlparse
- import sqlite3
- import xml.dom.minidom as minidom
- import other.utils as utils
- import gamespy.gs_database as gs_database
- import dwc_config
- from io import BytesIO
- # Paths to ProxyPass: /SakeStorageServer, /SakeFileServer
- logger = dwc_config.get_logger('StorageServer')
- address = dwc_config.get_ip_port('StorageServer')
- def escape_xml(s):
- s = s.replace( "&", "&" )
- s = s.replace( '"', """ )
- s = s.replace( "'", "'" )
- s = s.replace( "<", "<" )
- s = s.replace( ">", ">" )
- 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)
- self.gamespydb = gs_database.GamespyDatabase()
- self.db = sqlite3.connect('storage.db')
- self.tables = {}
- self.valid_sql_terms = ['LIKE', '=', 'AND', 'OR']
- logger.log(logging.INFO, "Checking for and creating database tables...")
- cursor = self.db.cursor()
- if not self.table_exists('typedata'):
- cursor.execute('CREATE TABLE typedata (tbl TEXT, col TEXT, type TEXT)')
- if not self.table_exists('filepaths'):
- cursor.execute('CREATE TABLE filepaths (fileid INTEGER PRIMARY KEY AUTOINCREMENT, gameid INT, playerid INT, path TEXT)')
- PK = 'INTEGER PRIMARY KEY AUTOINCREMENT'
- self.create_or_alter_table_if_not_exists(
- 'g1443_bbdx_player',
- ['recordid', 'stat'],
- [PK, 'INT' ],
- ['int', 'int' ])
- self.create_or_alter_table_if_not_exists(
- 'g1443_bbdx_info',
- ['serialid', 'stat', 'message' ],
- ['INT', 'INT', 'TEXT' ],
- ['int', 'int', 'unicodeString'])
- self.create_or_alter_table_if_not_exists(
- 'g1443_bbdx_search',
- ['recordid', 'song_name', 'creator_name', 'average_rating', 'serialid', 'filestore', 'is_lyric', 'num_ratings', 'jasrac_code', 'artist_name'],
- [PK, 'TEXT', 'TEXT', 'REAL', 'INT', 'INT', 'INT', 'INT', 'TEXT', 'TEXT' ],
- ['int', 'asciiString', 'asciiString', 'float', 'int', 'int', 'boolean', 'int', 'asciiString', 'asciiString'])
- # Mario Kart Wii
- self.create_or_alter_table_if_not_exists(
- 'g1687_FriendInfo',
- ['recordid', 'ownerid', 'info' ],
- [PK, 'INT', 'TEXT' ],
- ['int', 'int', 'binaryData'])
- self.create_or_alter_table_if_not_exists(
- 'g1687_StoredGhostData',
- ['recordid', 'fileid', 'profile', 'region', 'gameid', 'course' ],
- [PK, 'INT', 'INT', 'INT', 'INT', 'INT' ],
- ['int', 'int', 'int', 'int', 'int', 'int' ])
- # WarioWare DIY
- self.create_or_alter_table_if_not_exists(
- 'g2050_contest',
- ['recordid', 'ownerid', 'm_no', 'm_file_id'],
- [PK, 'INT', 'INT', 'INT' ],
- ['int', 'int', 'int', 'int' ])
- self.create_or_alter_table_if_not_exists(
- 'g2050_contest_eu',
- ['recordid', 'ownerid', 'm_no', 'm_file_id'],
- [PK, 'INT', 'INT', 'INT' ],
- ['int', 'int', 'int', 'int' ])
- self.create_or_alter_table_if_not_exists(
- 'g2050_contest_us',
- ['recordid', 'ownerid', 'm_no', 'm_file_id'],
- [PK, 'INT', 'INT', 'INT' ],
- ['int', 'int', 'int', 'int' ])
- self.create_or_alter_table_if_not_exists(
- 'g2050_box',
- ['recordid', 'ownerid', 'm_enable', 'm_type', 'm_index', 'm_file_id', 'm_header', 'm_file_id___size', 'm_file_id___create_time', 'm_file_id___downloads'],
- [PK, 'INT', 'INT', 'INT', 'INT', 'INT', 'TEXT', 'INT', 'DATETIME', 'INT' ],
- ['int', 'int', 'boolean', 'int', 'int', 'int', 'binaryData', 'int', 'dateAndTime', 'int' ])
- cursor.execute('CREATE TRIGGER IF NOT EXISTS g2050ti_box AFTER INSERT ON g2050_box BEGIN UPDATE g2050_box SET m_file_id___create_time = strftime(\'%Y-%m-%dT%H:%M:%f\', \'now\'), m_file_id___size = 0, m_file_id___downloads = 0 WHERE recordid = NEW.recordid; END')
- cursor.execute('CREATE TRIGGER IF NOT EXISTS g2050tu_box AFTER UPDATE ON g2050_box BEGIN UPDATE g2050_box SET m_file_id___create_time = strftime(\'%Y-%m-%dT%H:%M:%f\', \'now\') WHERE recordid = NEW.recordid; END')
- self.create_or_alter_table_if_not_exists(
- 'g2050_box_us_eu',
- ['recordid', 'ownerid', 'm_enable', 'm_type', 'm_index', 'm_file_id', 'm_header', 'm_file_id___size', 'm_file_id___create_time', 'm_file_id___downloads'],
- [PK, 'INT', 'INT', 'INT', 'INT', 'INT', 'TEXT', 'INT', 'DATETIME', 'INT' ],
- ['int', 'int', 'boolean', 'int', 'int', 'int', 'binaryData', 'int', 'dateAndTime', 'int' ])
- cursor.execute('CREATE TRIGGER IF NOT EXISTS g2050ti_box_us_eu AFTER INSERT ON g2050_box_us_eu BEGIN UPDATE g2050_box_us_eu SET m_file_id___create_time = strftime(\'%Y-%m-%dT%H:%M:%f\', \'now\'), m_file_id___size = 0, m_file_id___downloads = 0 WHERE recordid = NEW.recordid; END')
- cursor.execute('CREATE TRIGGER IF NOT EXISTS g2050tu_box_us_eu AFTER UPDATE ON g2050_box_us_eu BEGIN UPDATE g2050_box_us_eu SET m_file_id___create_time = strftime(\'%Y-%m-%dT%H:%M:%f\', \'now\') WHERE recordid = NEW.recordid; END')
- self.create_or_alter_table_if_not_exists(
- 'g2649_bbdx_player',
- ['recordid', 'stat'],
- [PK, 'INT' ],
- ['int', 'int' ])
- self.create_or_alter_table_if_not_exists(
- 'g2649_bbdx_info',
- ['serialid', 'stat', 'message' ],
- ['INT', 'INT', 'TEXT' ],
- ['int', 'int', 'unicodeString'])
- self.create_or_alter_table_if_not_exists(
- 'g2649_bbdx_search',
- ['recordid', 'song_name', 'creator_name', 'average_rating', 'serialid', 'filestore', 'is_lyric', 'num_ratings', 'song_code', 'artist_name'],
- [PK, 'TEXT', 'TEXT', 'REAL', 'INT', 'INT', 'INT', 'INT', 'TEXT', 'TEXT' ],
- ['int', 'asciiString', 'asciiString', 'float', 'int', 'int', 'boolean', 'int', 'asciiString', 'asciiString'])
- # Playground
- self.create_or_alter_table_if_not_exists(
- 'g2999_tblRegionInfo',
- ['recordid', 'region', 'allowed_regions', 'min_ratings' ],
- [PK, 'INT', 'INT', 'INT' ],
- ['int', 'byte', 'int', 'int' ])
- # Super Smash Bros. Brawl
- self.create_or_alter_table_if_not_exists(
- 'g1658_submit',
- ['recordid', 'ownerid', 'data'],
- [PK, 'INT', 'INT'],
- ['int', 'int', 'int']
- )
- self.create_or_alter_table_if_not_exists(
- 'g1658_watching',
- ['recordid', 'ownerid', 'data'],
- [PK, 'INT', 'INT'],
- ['int', 'int', 'int']
- )
- # Trackmania Wii
- self.create_or_alter_table_if_not_exists(
- 'g2793_player',
- ['recordid', 'ownerid', 'ladder', 'avatar', 'mii', 'name', 'wins', 'loses', 'count', 'row'],
- [PK, 'INT', 'INT', 'INT', 'INT', 'TEXT', 'INT', 'INT', 'INT', 'INT'],
- ['int', 'int', 'int', 'int', 'int', 'asciiString', 'int', 'int', 'int', 'int']
- )
- self.create_or_alter_table_if_not_exists(
- 'g2793_solo',
- ['recordid', 'ownerid', 'trackID', 'time', 'ghostID', 'ghostTime', 'date', 'ghostSize', 'row'],
- [PK, 'INT', 'INT', 'REAL', 'INT', 'REAL', 'INT', 'INT', 'INT'],
- ['int', 'int', 'int', 'float', 'int', 'float', 'int', 'int', 'int']
- )
- self.create_or_alter_table_if_not_exists(
- 'g2793_custom',
- ['recordid', 'ownerid', 'name', 'author', 'date', 'env', 'trackFile', 'trackSize', 'goldFile', 'goldSize', 'silverFile', 'silverSize', 'bronzeFile', 'bronzeSize', 'isValidated'],
- [PK, 'INT', 'TEXT', 'TEXT', 'INT', 'INT', 'INT', 'INT', 'INT', 'INT', 'INT', 'INT', 'INT', 'INT', 'INT'],
- ['int', 'int', 'asciiString', 'asciiString', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int']
- )
- self.create_or_alter_table_if_not_exists(
- 'g2793_customDLC',
- ['recordid', 'ownerid', 'name', 'author', 'date', 'env', 'trackFile', 'trackSize', 'goldFile', 'goldSize', 'silverFile', 'silverSize', 'bronzeFile', 'bronzeSize', 'isValidated', 'isDLC'],
- [PK, 'INT', 'TEXT', 'TEXT', 'INT', 'INT', 'INT', 'INT', 'INT', 'INT', 'INT', 'INT', 'INT', 'INT', 'INT', 'INT'],
- ['int', 'int', 'asciiString', 'asciiString', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int']
- )
- # load column info into memory, unfortunately there's no simple way
- # to check for column-existence so get that data in advance
- cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
- tabledata = cursor.fetchall()
- for t in tabledata:
- cursor.execute("PRAGMA table_info(%s)" % t[0]) # yeah I know but parameters don't work in pragmas, and inserting table names like that should be safe
- columns = cursor.fetchall()
- self.tables[t[0]] = [c[1] for c in columns]
- self.db.commit()
- def table_exists(self, name):
- cursor = self.db.cursor()
- cursor.execute("SELECT Count(1) FROM sqlite_master WHERE type='table' AND name=?", (name,))
- exists = cursor.fetchone()[0]
- return True if exists else False
- def column_exists(self, table, column):
- cursor = self.db.cursor()
- cursor.execute("PRAGMA table_info(%s)" % table)
- data = cursor.fetchall()
- for row in data:
- if column == row[1]:
- return True
- return False
- def create_or_alter_table_if_not_exists(self, table, columns, sqlTypes, sakeTypes):
- # this is certainly not the most efficient way to create a table, but it should work and makes that mess in the init function more readable
- if not self.table_exists(table):
- cursor = self.db.cursor()
- cursor.execute('CREATE TABLE ' + table + ' (' + columns[0] + ' ' + sqlTypes[0] + ')')
- cursor.execute('INSERT INTO typedata (tbl, col, type) VALUES (?, ?, ?)', (table, columns[0], sakeTypes[0] + 'Value'))
- for i, col in enumerate(columns):
- self.create_column_if_not_exists(table, columns[i], sqlTypes[i], sakeTypes[i] + 'Value')
- return
- def create_column_if_not_exists(self, table, column, sqlType, sakeType):
- if not self.column_exists(table, column):
- cursor = self.db.cursor()
- cursor.execute('ALTER TABLE ' + table + ' ADD COLUMN ' + column + ' ' + sqlType)
- cursor.execute('INSERT INTO typedata (tbl, col, type) VALUES (?, ?, ?)', (table, column, sakeType))
- return
- def get_typedata(self, table, column):
- try:
- cursor = self.db.cursor()
- cursor.execute("SELECT type FROM typedata WHERE tbl=? AND col=?", (table,column))
- return cursor.fetchone()[0]
- except TypeError:
- return 'UNKNOWN'
- class IllegalColumnAccessException(Exception):
- pass
- class FilterSyntaxException(Exception):
- pass
- class StorageHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
- def confirm_columns(self, columndata, table):
- '''Check if the columns the user wants to access actually exist, which should prevent SQL Injection'''
- columns = []
- for c in columndata:
- colname = c.firstChild.data.replace('.', '___') # fake the attributes that the actual sake databases have
- if colname not in self.server.tables[table]:
- raise IllegalColumnAccessException("Unknown column access '%s' in table '%s'" % (colname, table))
- columns.append(colname)
- return columns
- def tokenize_filter(self, filter):
- # TODO: Actual proper tokenization
- return filter.split()
- def parse_filter(self, table, filter):
- # I think I need to read up on how to properly parse SQL-like data, but this should do for the stuff I've seen from games so far.
- out = ''
- if ';' in filter:
- raise FilterSyntaxException("Semicolon in filter '%s'" & filter)
- if '\\' in filter:
- raise FilterSyntaxException("Backslash in filter '%s'" & filter)
- brace_count = filter.count('(')
- if brace_count != filter.count(')'):
- raise FilterSyntaxException("Mismatching brace count in filter '%s'" & filter)
- filter = self.tokenize_filter(filter)
- for f in filter:
- if f in self.server.tables[table]:
- # is a table name
- out += f + ' '
- elif f.upper() in self.server.valid_sql_terms:
- # is some SQL term such as LIKE, AND, OR, etc.
- out += f + ' '
- elif ( f.startswith("'") and f.endswith("'") ) or ( f.startswith('"') and f.endswith('"') ):
- # is a string
- out += f + ' '
- else:
- # is nothing valid, abort and return the statement so far
- out = out.strip()
- # try to make the output still valid by removing trailing connecting tokens
- last_space = out.rfind(' ')
- if last_space >= 0:
- last_token = out[last_space + 1 : ]
- if last_token in self.server.valid_sql_terms:
- out = out[ : last_space ]
- return out
- return out
- def append_filter(self, filter, table, statement, where_appended):
- try:
- filters = self.parse_filter(table, filter)
- if filters:
- if not where_appended:
- statement += ' WHERE '
- where_appended = True
- else:
- statement += ' AND '
- statement += ' ( '
- statement += filters
- statement += ' ) '
- except FilterSyntaxException as e:
- logger.log(logging.WARNING, "FilterSyntaxException: %s by %s", e.message, self.client_address)
- pass
- return statement, where_appended
- def do_POST(self):
- # Alright, in case anyone is wondering: Yes, I am faking a SOAP service
- # instead of using an actual one. That's because I've tried to do this
- # with several actual python SOAP services and none of them give me the
- # ability to return the exact format that I want.
- # (Or make any kind of sense in case of ZSI...)
- if self.path == "/SakeStorageServer/StorageServer.asmx":
- length = int(self.headers.get('content-length', -1))
- action = self.headers['SOAPAction']
- post = self.rfile.read(length)
- logger.log(logging.DEBUG, "SakeStorageServer SOAPAction %s", action)
- logger.log(logging.DEBUG, post)
- shortaction = action[action.rfind('/')+1:-1]
- ret = '<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body>'
- if "<!DOCTYPE" in post.upper():
- logger.log(logging.ERROR, "User tried to redefine a DOCTYPE")
- return
- dom = minidom.parseString(post)
- data = dom.getElementsByTagName('SOAP-ENV:Body')[0].getElementsByTagName('ns1:' + shortaction)[0]
- gameid = str(int(data.getElementsByTagName('ns1:gameid')[0].firstChild.data))
- tableid = data.getElementsByTagName('ns1:tableid')[0].firstChild.data
- loginticket = data.getElementsByTagName('ns1:loginTicket')[0].firstChild.data
- table = 'g' + gameid + '_' + tableid
- if not self.server.table_exists(table):
- logger.log(logging.WARNING, "Unknown table access '%s' in %s by %s", table, shortaction, self.client_address)
- return
- ret += '<' + shortaction + 'Response xmlns="http://gamespy.net/sake">'
- ret += '<' + shortaction + 'Result>Success</' + shortaction + 'Result>'
- if shortaction == 'SearchForRecords' or shortaction == 'GetMyRecords' or shortaction == 'GetSpecificRecords':
- columndata = data.getElementsByTagName('ns1:fields')[0].getElementsByTagName('ns1:string')
- try:
- columns = self.confirm_columns(columndata, table)
- except IllegalColumnAccessException as e:
- logger.log(logging.WARNING, "IllegalColumnAccess: %s in %s by %s", e.message, shortaction, self.client_address)
- return
- # build SELECT statement, yes I know one shouldn't do this but I cross-checked the table name and all the columns above so it should be fine
- statement = 'SELECT '
- statement += ",".join(columns)
- statement += ' FROM ' + table
- where_appended = False
- if shortaction == 'SearchForRecords':
- # this is ugly as hell but SearchForRecords can request specific ownerids like this
- owneriddata = data.getElementsByTagName('ns1:ownerids')
- if owneriddata and owneriddata[0] and owneriddata[0].firstChild:
- oids = owneriddata[0].getElementsByTagName('ns1:int')
- if not where_appended:
- statement += ' WHERE '
- where_appended = True
- else:
- statement += ' AND '
- statement += ' ( '
- statement += ' OR '.join('ownerid = '+str(int(oid.firstChild.data)) for oid in oids)
- statement += ' ) '
- elif shortaction == 'GetMyRecords':
- profileid = self.server.gamespydb.get_profileid_from_loginticket(loginticket)
- if not where_appended:
- statement += ' WHERE '
- where_appended = True
- else:
- statement += ' AND '
- statement += ' ( ownerid = ' + str(profileid) + ' ) '
- elif shortaction == 'GetSpecificRecords':
- recordids = data.getElementsByTagName('ns1:recordids')[0].getElementsByTagName('ns1:int')
- # limit to requested records
- if not where_appended:
- statement += ' WHERE '
- where_appended = True
- else:
- statement += ' AND '
- statement += ' ( '
- statement += ' OR '.join('recordid = '+str(int(r.firstChild.data)) for r in recordids)
- statement += ' ) '
- # if there's a filter, evaluate it
- filterdata = data.getElementsByTagName('ns1:filter')
- if filterdata and filterdata[0] and filterdata[0].firstChild:
- statement, where_appended = self.append_filter(filterdata[0].firstChild.data, table, statement, where_appended)
- # if only a subset of the data is wanted
- limit_offset_data = data.getElementsByTagName('ns1:offset')
- limit_max_data = data.getElementsByTagName('ns1:max')
- limits = []
- if limit_offset_data and limit_offset_data[0] and limit_offset_data[0].firstChild:
- limits.append(str(int(limit_offset_data[0].firstChild.data)))
- if limit_max_data and limit_max_data[0] and limit_max_data[0].firstChild:
- limits.append(str(int(limit_max_data[0].firstChild.data)))
- if limits:
- statement += ' LIMIT ' + ','.join(limits)
- logger.log(logging.DEBUG, statement)
- cursor = self.server.db.cursor()
- cursor.execute(statement)
- rows = cursor.fetchall()
- if rows:
- ret += '<values>'
- for r in rows:
- ret += '<ArrayOfRecordValue>'
- for i, c in enumerate(r):
- type = self.server.get_typedata(table, columns[i])
- ret += '<RecordValue>'
- ret += '<' + type + '>'
- if c is not None:
- if type == 'booleanValue':
- ret += '<value>' + ('true' if c else 'false') + '</value>'
- else:
- ret += '<value>' + escape_xml(str(c)) + '</value>'
- else:
- ret += '<value/>'
- ret += '</' + type + '>'
- ret += '</RecordValue>'
- ret += '</ArrayOfRecordValue>'
- ret += '</values>'
- else:
- ret += '<values/>'
- elif shortaction == 'GetRecordCount':
- statement = 'SELECT COUNT(1) FROM ' + table
- filterdata = data.getElementsByTagName('ns1:filter')
- if filterdata and filterdata[0] and filterdata[0].firstChild:
- statement, where_appended = self.append_filter(filterdata[0].firstChild.data, table, statement, False)
- logger.log(logging.DEBUG, statement)
- cursor = self.server.db.cursor()
- cursor.execute(statement)
- count = cursor.fetchone()[0]
- ret += '<count>' + str(count) + '</count>'
- elif shortaction == 'UpdateRecord' or shortaction == 'CreateRecord':
- if shortaction == 'UpdateRecord':
- recordid = int(data.getElementsByTagName('ns1:recordid')[0].firstChild.data)
- profileid = self.server.gamespydb.get_profileid_from_loginticket(loginticket)
- values = data.getElementsByTagName('ns1:values')[0]
- recordfields = values.getElementsByTagName('ns1:RecordField')
- columndata = [rf.getElementsByTagName('ns1:name')[0]
- for rf in recordfields]
- try:
- columns = self.confirm_columns(columndata, table)
- except IllegalColumnAccessException as e:
- logger.log(logging.WARNING, "IllegalColumnAccess: %s in %s by %s", e.message, shortaction, self.client_address)
- return
- rowdata = []
- for i, rf in enumerate(recordfields):
- type = self.server.get_typedata(table, columns[i])
- value = rf.getElementsByTagName('ns1:value')[0].getElementsByTagName('ns1:' + type)[0].getElementsByTagName('ns1:value')[0].firstChild.data
- if type == 'intValue' or type == 'booleanValue':
- rowdata.append( int(value) )
- elif type == 'floatValue':
- rowdata.append( float(value) )
- else:
- rowdata.append( value )
- if shortaction == 'UpdateRecord':
- statement = 'UPDATE ' + table + ' SET '
- statement += ', '.join(c+' = ?' for c in columns)
- statement += ' WHERE recordid = ? AND ownerid = ?'
- rowdata.append( recordid )
- rowdata.append( profileid )
- elif shortaction == 'CreateRecord':
- statement = 'INSERT INTO ' + table + ' ('
- statement += ', '.join(columns)
- statement += ', ownerid) VALUES ('
- statement += '?, '*len(columns)
- statement += '?)'
- rowdata.append( profileid )
- else:
- logger.log(logging.ERROR, 'Illegal Action %s in database insert/update path!', shortaction)
- return
- cursor = self.server.db.cursor()
- cursor.execute(statement, tuple(rowdata))
- recordid = cursor.lastrowid
- if shortaction == 'CreateRecord':
- ret += '<recordid>' + str(recordid) + '</recordid>'
- # Alright, so this kinda sucks, but we have no good way of automatically inserting
- # or updating the file's .size attribute, so we have to manually check if any column
- # has that, and update it accordingly.
- # I have no idea if this will work with all games but it seems to work in WarioWare.
- for i, col in enumerate(columns):
- attrcol = col + '___size'
- if attrcol in self.server.tables[table]:
- if rowdata[i] == 0: # is a delete command, just set filesize to 0
- filesize = 0
- else:
- cursor.execute('SELECT path FROM filepaths WHERE fileid = ?', (int(rowdata[i]),))
- try:
- filename = cursor.fetchone()[0]
- filesize = os.path.getsize(filename)
- except:
- filesize = 0
- cursor.execute('UPDATE ' + table + ' SET ' + attrcol + ' = ? WHERE recordid = ?', (filesize, recordid))
- self.server.db.commit()
- ret += '</' + shortaction + 'Response>'
- ret += '</soap:Body></soap:Envelope>'
- self.send_response(200)
- self.send_header('Content-Type', 'text/xml; charset=utf-8')
- self.end_headers()
- logger.log(logging.DEBUG, "%s response to %s", action, self.client_address)
- #logger.log(logging.DEBUG, ret)
- self.wfile.write(ret)
- elif self.path.startswith("/SakeFileServer/upload.aspx?"):
- retcode = 0
- params = urlparse.parse_qs(self.path[self.path.find('?')+1:])
- gameid = int(params['gameid'][0])
- playerid = int(params['pid'][0])
- logger.log(logging.DEBUG, "SakeFileServer Upload Request in game %s, user %s", gameid, playerid)
- ctype, pdict = cgi.parse_header(self.headers['Content-Type'])
- multipart_data = self.rfile.read(int(self.headers.get('Content-Length', -1)))
- filedata = cgi.parse_multipart(BytesIO(multipart_data), pdict)
- data = filedata.get('data')
- if data is not None:
- data = data[0]
- else:
- for key in filedata:
- if not filedata[key]:
- continue
- data = filedata[key][0]
- break
- filesize = -1 if data is None else len(data)
- # make sure users don't upload huge files, dunno what an actual sensible maximum is
- # but 64 KB seems reasonable for what I've seen in WarioWare
- if data is not None and filesize <= 65536:
- # Apparently the real Sake doesn't care about the gameid/playerid, just the fileid
- # but for better categorization I think I'm still gonna leave folder-per-game/player thing
- userdir = 'usercontent/' + str(gameid) + '/' + str(playerid)
- if not os.path.exists(userdir):
- os.makedirs(userdir)
- # get next fileid from database
- cursor = self.server.db.cursor()
- cursor.execute('INSERT INTO filepaths (gameid, playerid) VALUES (?, ?)', (gameid, playerid))
- fileid = cursor.lastrowid
- path = userdir + '/' + str(fileid)
- cursor.execute('UPDATE filepaths SET path = ? WHERE fileid = ?', (path, fileid))
- with open(path, 'wb') as fi:
- fi.write(data)
- elif data is not None:
- logger.log(logging.WARNING, "Tried to upload big file, rejected. (%s bytes)", filesize)
- fileid = 0
- retcode = 1
- else:
- logger.log(logging.ERROR, "Failed to read data")
- fileid = 0
- retcode = 1
- self.send_response(200)
- if retcode == 0:
- self.send_header('Sake-File-Id', str(fileid))
- self.send_header('Sake-File-Result', str(retcode))
- self.end_headers()
- logger.log(logging.DEBUG, "SakeFileServer Upload Reply Sake-File-Id %s (%d bytes)", fileid, filesize)
- self.wfile.write('')
- else:
- logger.log(logging.INFO, "[NOT IMPLEMENTED] Got POST request %s from %s", self.path, self.client_address)
- def do_GET(self):
- if self.path.startswith("/SakeFileServer/download.aspx?"):
- params = urlparse.parse_qs(self.path[self.path.find('?')+1:])
- retcode = 0
- ret = ''
- if 'pid' not in params or 'fileid' not in params or 'gameid' not in params:
- logger.log(logging.DEBUG, "Could not find expected parameters")
- retcode = 3
- else:
- fileid = int(params['fileid'][0])
- gameid = int(params['gameid'][0])
- playerid = int(params['pid'][0])
- logger.log(logging.DEBUG, "SakeFileServer Download Request in game %s, user %s, file %s", gameid, playerid, fileid)
- cursor = self.server.db.cursor()
- cursor.execute('SELECT path FROM filepaths WHERE fileid = ?', (fileid,))
- try:
- filename = cursor.fetchone()[0]
- if os.path.exists(filename):
- with open(filename, 'rb') as fi:
- ret = fi.read()
- else:
- logger.log(logging.ERROR, "User is trying to access file that should exist according to DB, but doesn't! (%s)", filename)
- except:
- logger.log(logging.WARNING, "User is trying to access non-existing file!")
- ret = '1234' # apparently some games use the download command just to increment the "downloads" counter, and get the actual file from dls1
- #retcode = 4
- filelen = len(ret)
- self.send_response(200)
- self.send_header('Sake-File-Result', str(retcode))
- self.send_header('Content-Type', 'text/html')
- self.send_header('Content-Length', filelen)
- self.end_headers()
- logger.log(logging.DEBUG, "Returning download request with file of %s bytes", filelen)
- self.wfile.write(ret)
- else:
- logger.log(logging.INFO, "[NOT IMPLEMENTED] Got GET request %s from %s", self.path, self.client_address)
- if __name__ == "__main__":
- storage_server = StorageServer()
- storage_server.start()
|