Ported GTS Phase I over from spline-pokedex.
[zzz-spline-gts.git] / spline / plugins / gts / controllers / gts.py
1 # encoding: utf8
2 from __future__ import absolute_import, division
3
4 from base64 import urlsafe_b64decode
5 from collections import namedtuple
6 from itertools import izip
7 import logging
8 from random import sample
9 from string import uppercase, lowercase, digits
10 import struct
11
12 import pokedex.db
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
22
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
27
28 from spline.plugins.gts.model import GTSPokemon
29
30 log = logging.getLogger(__name__)
31
32
33 ### Utility functions
34
35 def gts_prng(seed):
36 """Implements the GTS's linear congruential generator.
37
38 I love typing that out. It makes me sounds so smart.
39
40 Yields magical numbers."""
41 # 0xabcd => 0xabcdabcd
42 seed = seed | (seed << 16)
43 while True:
44 seed = (seed * 0x45 + 0x1111) & 0x7fffffff # signed dword!
45 yield (seed >> 16) & 0xff
46
47 def stream_decipher(data, keystream):
48 """Reverses a stream cipher, given iterable data and keystream.
49
50 Yields decrypted bytes.
51 """
52 for c, key in izip(data, keystream):
53 new_c = (ord(c) ^ key) & 0xff
54 yield chr(new_c)
55
56
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
60 intelligible.
61 """
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
68
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)) )
71
72
73 ### Controller!
74
75 def dbg(*args):
76 #print ' '.join(args)
77 pass
78
79 class GTSController(BaseController):
80
81 def dispatch(self, page):
82 """We do our own dispatching for two reasons:
83
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
86 every action.
87
88 2. Easier to dump stuff if desired.
89 """
90
91 dbg(request)
92
93 # Always return binary!
94 response.headers['Content-type'] = 'application/octet-stream'
95
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
100 # junk, anyway.
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
104 else:
105 data = ''
106
107 method = 'page_' + page
108 if not hasattr(self, method):
109 dbg("NOT YET IMPLEMENTED?")
110 abort(404)
111
112 res = getattr(self, method)(request.params['pid'], data)
113 dbg("RESPONDING WITH ", type(res), len(res), repr(res))
114 return res
115
116 # No hash. Need to issue a challenge. It's random, so whatever
117 return ''.join(sample( uppercase + lowercase + digits, 32 ))
118
119 ### Actual pages
120
121 def page_info(self, pid, data):
122 """info.asp
123
124 Apparently always returns 0x0001. Probably just a ping to see if the
125 server is up.
126 """
127
128 return '\x01\x00'
129
130 def page_setProfile(self, pid, data):
131 """setProfile.asp
132
133 Only Pt and later check this page. Don't know why. It returns eight
134 NULs, every time.
135 """
136
137 return '\x00' * 8
138
139 def page_result(self, pid, data):
140 u"""result.asp
141
142 The good part!
143
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.
147
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
150 ol' Pokémon blob.
151 """
152
153 # Check for an existing Pokémon
154 # TODO support multiple!
155 try:
156 stored_pokemon = meta.Session.query(model.GTSPokemon) \
157 .filter_by(pid=pid) \
158 .one()
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
163
164 except:
165 # Nothing
166 return '\x05\x00'
167
168 def page_delete(self, pid, data):
169 u"""delete.asp
170
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.
173
174 Returns 0x0001.
175 """
176
177 meta.Session.query(model.GTSPokemon).filter_by(pid=pid).delete()
178 meta.Session.commit()
179
180 return '\x01\x00'
181
182 def page_post(self, pid, data):
183 u"""post.asp
184
185 Deposits a Pokémon in the GTS. Returns 0x0001 on success, or 0x000c if
186 the deposit is rejected.
187 """
188
189 try:
190 # The uploaded Pokémon is encrypted, which is not very useful
191 pokemon_save = PokemonSave(data, encrypted=True)
192
193 # Create a record...
194 stored_pokemon = model.GTSPokemon(
195 pid=pid,
196 pokemon_blob=pokemon_save.as_struct,
197 )
198 meta.Session.add(stored_pokemon)
199 meta.Session.commit()
200 return '\x01\x00'
201 except:
202 # If that failed (presumably due to unique key collision), we're
203 # already storing something. Reject!
204 return '\x0c\x00'
205
206 def page_post_finish(self, pid, data):
207 u"""post_finish.asp
208
209 Surely this does something, but for the life of me I can't figure out
210 what.
211
212 Returns 0x0001.
213 """
214 return '\x01\x00'