From: Eevee Date: Fri, 30 Apr 2010 05:07:59 +0000 (-0700) Subject: Ported GTS Phase I over from spline-pokedex. X-Git-Tag: veekun-promotions/2010050901^0 X-Git-Url: http://git.veekun.com/zzz-spline-gts.git/commitdiff_plain/b857c1e49fbc463e8c25c85fc0ec443d7c7d653f Ported GTS Phase I over from spline-pokedex. --- b857c1e49fbc463e8c25c85fc0ec443d7c7d653f diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6df398f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.swp +*.pyc + +*.egg-info/ +/*.db +/data +/migration.py diff --git a/migration/README b/migration/README new file mode 100644 index 0000000..6218f8c --- /dev/null +++ b/migration/README @@ -0,0 +1,4 @@ +This is a database migration repository. + +More information at +http://code.google.com/p/sqlalchemy-migrate/ diff --git a/migration/__init__.py b/migration/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/migration/manage.py b/migration/manage.py new file mode 100644 index 0000000..0add9c2 --- /dev/null +++ b/migration/manage.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +from migrate.versioning.shell import main + +main(repository='migration') diff --git a/migration/migrate.cfg b/migration/migrate.cfg new file mode 100644 index 0000000..2deb5c1 --- /dev/null +++ b/migration/migrate.cfg @@ -0,0 +1,20 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id=spline-gts + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table=migrate_version + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] +required_dbs=[] diff --git a/migration/versions/001_Create_a_permanent_deposits_table.py b/migration/versions/001_Create_a_permanent_deposits_table.py new file mode 100644 index 0000000..d019e7d --- /dev/null +++ b/migration/versions/001_Create_a_permanent_deposits_table.py @@ -0,0 +1,19 @@ +from sqlalchemy import * +from migrate import * + +from sqlalchemy.ext.declarative import declarative_base +TableBase = declarative_base(bind=migrate_engine) + + +class GTSPokemon(TableBase): + __tablename__ = 'gts_pokemon' + id = Column(Integer, primary_key=True, autoincrement=True) + pid = Column(Integer) + pokemon_blob = Column(Binary(292), nullable=False) + + +def upgrade(): + GTSPokemon.__table__.create() + +def downgrade(): + GTSPokemon.__table__.drop() diff --git a/migration/versions/__init__.py b/migration/versions/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7dd0cfc --- /dev/null +++ b/setup.py @@ -0,0 +1,20 @@ +from setuptools import setup, find_packages +setup( + name = 'spline-gts', + version = '0.1', + packages = find_packages(), + + install_requires = [ + 'spline', + 'spline-users', + 'pokedex', + ], + + include_package_data = True, + + zip_safe = False, + + entry_points = {'spline.plugins': 'gts = spline.plugins.gts:GTSPlugin'}, + + namespace_packages = ['spline', 'spline.plugins'], +) diff --git a/spline/__init__.py b/spline/__init__.py new file mode 100644 index 0000000..de40ea7 --- /dev/null +++ b/spline/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/spline/plugins/__init__.py b/spline/plugins/__init__.py new file mode 100644 index 0000000..de40ea7 --- /dev/null +++ b/spline/plugins/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/spline/plugins/gts/__init__.py b/spline/plugins/gts/__init__.py new file mode 100644 index 0000000..c1b0fb5 --- /dev/null +++ b/spline/plugins/gts/__init__.py @@ -0,0 +1,39 @@ +from pkg_resources import resource_filename + +from pylons import c, session + +from spline.lib.plugin import PluginBase +from spline.lib.plugin import PluginBase, PluginLink, Priority +import spline.model as model +import spline.model.meta as meta + +import spline.plugins.gts.controllers.gts +import spline.plugins.gts.model + +def add_routes_hook(map, *args, **kwargs): + """Hook to inject some of our behavior into the routes configuration.""" + # These are the GTS URLs + map.connect('/pokemondpds/worldexchange/{page}.asp', controller='gts', action='dispatch') + map.connect('/pokemondpds/common/{page}.asp', controller='gts', action='dispatch') + + +class GTSPlugin(PluginBase): + def controllers(self): + return dict( + gts = spline.plugins.gts.controllers.gts.GTSController, + ) + + def model(self): + return [ + spline.plugins.gts.model.GTSPokemon, + ] + + def template_dirs(self): + return [ + (resource_filename(__name__, 'templates'), Priority.NORMAL) + ] + + def hooks(self): + return [ + ('routes_mapping', Priority.NORMAL, add_routes_hook), + ] diff --git a/spline/plugins/gts/controllers/__init__.py b/spline/plugins/gts/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/spline/plugins/gts/controllers/gts.py b/spline/plugins/gts/controllers/gts.py new file mode 100644 index 0000000..98526a8 --- /dev/null +++ b/spline/plugins/gts/controllers/gts.py @@ -0,0 +1,214 @@ +# encoding: utf8 +from __future__ import absolute_import, division + +from base64 import urlsafe_b64decode +from collections import namedtuple +from itertools import izip +import logging +from random import sample +from string import uppercase, lowercase, digits +import struct + +import pokedex.db +import pokedex.db.tables as tables +import pokedex.formulae +from pokedex.savefile import PokemonSave +from pylons import config, request, response, session, tmpl_context as c, url +from pylons.controllers.util import abort, redirect_to +from sqlalchemy import and_, or_, not_ +from sqlalchemy.orm import aliased, contains_eager, eagerload, eagerload_all, join +from sqlalchemy.orm.exc import NoResultFound +from sqlalchemy.sql import func + +from spline import model +from spline.model import meta +from spline.lib.base import BaseController, render +from spline.lib import helpers as h + +from spline.plugins.gts.model import GTSPokemon + +log = logging.getLogger(__name__) + + +### Utility functions + +def gts_prng(seed): + """Implements the GTS's linear congruential generator. + + I love typing that out. It makes me sounds so smart. + + Yields magical numbers.""" + # 0xabcd => 0xabcdabcd + seed = seed | (seed << 16) + while True: + seed = (seed * 0x45 + 0x1111) & 0x7fffffff # signed dword! + yield (seed >> 16) & 0xff + +def stream_decipher(data, keystream): + """Reverses a stream cipher, given iterable data and keystream. + + Yields decrypted bytes. + """ + for c, key in izip(data, keystream): + new_c = (ord(c) ^ key) & 0xff + yield chr(new_c) + + +def decrypt_data(data): + """Takes a binary blob uploaded from a game and returns the original binary + blob. Depending on your perspective, the returned value may be more + intelligible. + """ + # GTS encryption is a simple stream cipher. + # The first four bytes of the data are a header containing an obfuscated + # key; the rest is the message + obf_key_blob, message = data[0:4], data[4:] + obf_key, = struct.unpack('>I', obf_key_blob) + key = obf_key ^ 0x4a3b2c1d + + # Data is XORed with the output of an LCG, like everything else in Pokémon + return ''.join( stream_decipher(message, gts_prng(key)) ) + + +### Controller! + +def dbg(*args): + #print ' '.join(args) + pass + +class GTSController(BaseController): + + def dispatch(self, page): + """We do our own dispatching for two reasons: + + 1. All requests do challenge/response before anything, and it's easier + to do that and then dispatch than copy/paste some block of code to + every action. + + 2. Easier to dump stuff if desired. + """ + + dbg(request) + + # Always return binary! + response.headers['Content-type'] = 'application/octet-stream' + + if 'hash' in request.params: + # Okay, already done the response. Decrypt, then dispatch! + if request.params['data']: + # Note: base64 doesn't like unicode. Go figure. It's binary + # junk, anyway. + encrypted_data = urlsafe_b64decode(str(request.params['data'])) + data = decrypt_data(encrypted_data) + data = data[4:] # data always starts with pid; we don't care + else: + data = '' + + method = 'page_' + page + if not hasattr(self, method): + dbg("NOT YET IMPLEMENTED?") + abort(404) + + res = getattr(self, method)(request.params['pid'], data) + dbg("RESPONDING WITH ", type(res), len(res), repr(res)) + return res + + # No hash. Need to issue a challenge. It's random, so whatever + return ''.join(sample( uppercase + lowercase + digits, 32 )) + + ### Actual pages + + def page_info(self, pid, data): + """info.asp + + Apparently always returns 0x0001. Probably just a ping to see if the + server is up. + """ + + return '\x01\x00' + + def page_setProfile(self, pid, data): + """setProfile.asp + + Only Pt and later check this page. Don't know why. It returns eight + NULs, every time. + """ + + return '\x00' * 8 + + def page_result(self, pid, data): + u"""result.asp + + The good part! + + This checks the game's status on the GTS. If there's nothing + interesting to report, it returns 0x0005. If the game has a Pokémon up + on the GTS, it returns 0x0004. + + However... if the game has a Pokémon waiting to come to it, it returns + that entire Pokémon struct! No header, no encryption. Just a regular + ol' Pokémon blob. + """ + + # Check for an existing Pokémon + # TODO support multiple! + try: + stored_pokemon = meta.Session.query(model.GTSPokemon) \ + .filter_by(pid=pid) \ + .one() + # We've got one! Cool, send it back. The game will ask us to + # delete it after receiving successfully + pokemon_save = PokemonSave(stored_pokemon.pokemon_blob) + return pokemon_save.as_encrypted + + except: + # Nothing + return '\x05\x00' + + def page_delete(self, pid, data): + u"""delete.asp + + If a Pokémon is received from result.asp, the game requests this page + to confirm that the incoming Pokémon may be deleted. + + Returns 0x0001. + """ + + meta.Session.query(model.GTSPokemon).filter_by(pid=pid).delete() + meta.Session.commit() + + return '\x01\x00' + + def page_post(self, pid, data): + u"""post.asp + + Deposits a Pokémon in the GTS. Returns 0x0001 on success, or 0x000c if + the deposit is rejected. + """ + + try: + # The uploaded Pokémon is encrypted, which is not very useful + pokemon_save = PokemonSave(data, encrypted=True) + + # Create a record... + stored_pokemon = model.GTSPokemon( + pid=pid, + pokemon_blob=pokemon_save.as_struct, + ) + meta.Session.add(stored_pokemon) + meta.Session.commit() + return '\x01\x00' + except: + # If that failed (presumably due to unique key collision), we're + # already storing something. Reject! + return '\x0c\x00' + + def page_post_finish(self, pid, data): + u"""post_finish.asp + + Surely this does something, but for the life of me I can't figure out + what. + + Returns 0x0001. + """ + return '\x01\x00' diff --git a/spline/plugins/gts/model/__init__.py b/spline/plugins/gts/model/__init__.py new file mode 100644 index 0000000..3d73b54 --- /dev/null +++ b/spline/plugins/gts/model/__init__.py @@ -0,0 +1,11 @@ +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relation +from sqlalchemy.types import Binary, Integer, Unicode + +from spline.model.meta import TableBase + +class GTSPokemon(TableBase): + __tablename__ = 'gts_pokemon' + id = Column(Integer, primary_key=True, autoincrement=True) + pid = Column(Integer) + pokemon_blob = Column(Binary(292), nullable=False) diff --git a/spline/plugins/gts/templates/css/gts.mako b/spline/plugins/gts/templates/css/gts.mako new file mode 100644 index 0000000..e69de29