2 from __future__
import absolute_import
, division
4 from base64
import urlsafe_b64decode
5 from collections
import namedtuple
6 from itertools
import izip
8 from random
import sample
9 from string
import uppercase
, lowercase
, digits
13 import pokedex
.db
.tables
as tables
14 import pokedex
.formulae
15 from pokedex
.savefile
import PokemonSave
16 from pylons
import config
, request
, response
, session
, tmpl_context
as c
, url
17 from pylons
.controllers
.util
import abort
, redirect_to
18 from sqlalchemy
import and_
, or_
, not_
19 from sqlalchemy
.orm
import aliased
, contains_eager
, eagerload
, eagerload_all
, join
20 from sqlalchemy
.orm
.exc
import NoResultFound
21 from sqlalchemy
.sql
import func
23 from spline
import model
24 from spline
.model
import meta
25 from spline
.lib
.base
import BaseController
, render
26 from spline
.lib
import helpers
as h
28 from spline
.plugins
.gts
.model
import GTSPokemon
30 log
= logging
.getLogger(__name__
)
36 """Implements the GTS's linear congruential generator.
38 I love typing that out. It makes me sounds so smart.
40 Yields magical numbers."""
41 # 0xabcd => 0xabcdabcd
42 seed
= seed |
(seed
<< 16)
44 seed
= (seed
* 0x45 + 0x1111) & 0x7fffffff # signed dword!
45 yield (seed
>> 16) & 0xff
47 def stream_decipher(data
, keystream
):
48 """Reverses a stream cipher, given iterable data and keystream.
50 Yields decrypted bytes.
52 for c
, key
in izip(data
, keystream
):
53 new_c
= (ord(c
) ^ key
) & 0xff
57 def decrypt_data(data
):
58 """Takes a binary blob uploaded from a game and returns the original binary
59 blob. Depending on your perspective, the returned value may be more
62 # GTS encryption is a simple stream cipher.
63 # The first four bytes of the data are a header containing an obfuscated
64 # key; the rest is the message
65 obf_key_blob
, message
= data
[0:4], data
[4:]
66 obf_key
, = struct
.unpack('>I', obf_key_blob
)
67 key
= obf_key ^
0x4a3b2c1d
69 # Data is XORed with the output of an LCG, like everything else in Pokémon
70 return ''.join( stream_decipher(message
, gts_prng(key
)) )
79 class GTSController(BaseController
):
81 def dispatch(self
, page
):
82 """We do our own dispatching for two reasons:
84 1. All requests do challenge/response before anything, and it's easier
85 to do that and then dispatch than copy/paste some block of code to
88 2. Easier to dump stuff if desired.
93 # Always return binary!
94 response
.headers
['Content-type'] = 'application/octet-stream'
96 if 'hash' in request
.params
:
97 # Okay, already done the response. Decrypt, then dispatch!
98 if request
.params
['data']:
99 # Note: base64 doesn't like unicode. Go figure. It's binary
101 encrypted_data
= urlsafe_b64decode(str(request
.params
['data']))
102 data
= decrypt_data(encrypted_data
)
103 data
= data
[4:] # data always starts with pid; we don't care
107 method
= 'page_' + page
108 if not hasattr(self
, method
):
109 dbg("NOT YET IMPLEMENTED?")
112 res
= getattr(self
, method
)(request
.params
['pid'], data
)
113 dbg("RESPONDING WITH ", type(res
), len(res
), repr(res
))
116 # No hash. Need to issue a challenge. It's random, so whatever
117 return ''.join(sample( uppercase
+ lowercase
+ digits
, 32 ))
121 def page_info(self
, pid
, data
):
124 Apparently always returns 0x0001. Probably just a ping to see if the
130 def page_setProfile(self
, pid
, data
):
133 Only Pt and later check this page. Don't know why. It returns eight
139 def page_result(self
, pid
, data
):
144 This checks the game's status on the GTS. If there's nothing
145 interesting to report, it returns 0x0005. If the game has a Pokémon up
146 on the GTS, it returns 0x0004.
148 However... if the game has a Pokémon waiting to come to it, it returns
149 that entire Pokémon struct! No header, no encryption. Just a regular
153 # Check for an existing Pokémon
154 # TODO support multiple!
156 stored_pokemon
= meta
.Session
.query(model
.GTSPokemon
) \
157 .filter_by(pid
=pid
) \
159 # We've got one! Cool, send it back. The game will ask us to
160 # delete it after receiving successfully
161 pokemon_save
= PokemonSave(stored_pokemon
.pokemon_blob
)
162 return pokemon_save
.as_encrypted
168 def page_delete(self
, pid
, data
):
171 If a Pokémon is received from result.asp, the game requests this page
172 to confirm that the incoming Pokémon may be deleted.
177 meta
.Session
.query(model
.GTSPokemon
).filter_by(pid
=pid
).delete()
178 meta
.Session
.commit()
182 def page_post(self
, pid
, data
):
185 Deposits a Pokémon in the GTS. Returns 0x0001 on success, or 0x000c if
186 the deposit is rejected.
190 # The uploaded Pokémon is encrypted, which is not very useful
191 pokemon_save
= PokemonSave(data
, encrypted
=True)
194 stored_pokemon
= model
.GTSPokemon(
196 pokemon_blob
=pokemon_save
.as_struct
,
198 meta
.Session
.add(stored_pokemon
)
199 meta
.Session
.commit()
202 # If that failed (presumably due to unique key collision), we're
203 # already storing something. Reject!
206 def page_post_finish(self
, pid
, data
):
209 Surely this does something, but for the life of me I can't figure out