diff --git a/dwc_network_server_emulator.pyproj b/dwc_network_server_emulator.pyproj new file mode 100644 index 0000000..2c970c9 --- /dev/null +++ b/dwc_network_server_emulator.pyproj @@ -0,0 +1,60 @@ + + + + Debug + 2.0 + {1a9e8a26-6cea-4e78-835e-b043356ce360} + . + master_server.py + + + . + . + 00000000-0000-0000-0000-000000000000 + + + dwc_network_server_emulator + dwc_network_server_emulator + + + true + false + + + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dwc_network_server_emulator.sln b/dwc_network_server_emulator.sln new file mode 100644 index 0000000..f35768c --- /dev/null +++ b/dwc_network_server_emulator.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "dwc_network_server_emulator", "dwc_network_server_emulator.pyproj", "{1A9E8A26-6CEA-4E78-835E-B043356CE360}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1A9E8A26-6CEA-4E78-835E-B043356CE360}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A9E8A26-6CEA-4E78-835E-B043356CE360}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A9E8A26-6CEA-4E78-835E-B043356CE360}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A9E8A26-6CEA-4E78-835E-B043356CE360}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/storage_server.py b/storage_server.py index 6ef58ed..237fb24 100644 --- a/storage_server.py +++ b/storage_server.py @@ -41,6 +41,7 @@ class StorageHTTPServer(BaseHTTPServer.HTTPServer): 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...") @@ -190,6 +191,8 @@ class StorageHTTPServer(BaseHTTPServer.HTTPServer): class IllegalColumnAccessException(Exception): pass +class FilterSyntaxException(Exception): + pass class StorageHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler): def confirm_columns(self, columndata, table): @@ -204,6 +207,66 @@ class StorageHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler): 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 @@ -249,26 +312,49 @@ class StorageHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler): 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') - statement += ' WHERE ' + 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) - statement += ' WHERE ownerid = ' + str(profileid) + 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 - statement += ' WHERE ' + 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') @@ -311,7 +397,13 @@ class StorageHTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler): 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]