15b34575e7f41b9d7be9d72a58f6102aed14945e
[zzz-spline-gts.git] / splinext / 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.struct import SaveFilePokemon
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.model import meta
24 from spline.lib.base import BaseController, render
25 from spline.lib import helpers as h
26 from splinext.gts import model as gts_model
27
28 log = logging.getLogger(__name__)
29
30
31 ### Utility functions
32
33 def gts_prng(seed):
34 """Implements the GTS's linear congruential generator.
35
36 I love typing that out. It makes me sounds so smart.
37
38 Yields magical numbers."""
39 # 0xabcd => 0xabcdabcd
40 seed = seed | (seed << 16)
41 while True:
42 seed = (seed * 0x45 + 0x1111) & 0x7fffffff # signed dword!
43 yield (seed >> 16) & 0xff
44
45 def stream_decipher(data, keystream):
46 """Reverses a stream cipher, given iterable data and keystream.
47
48 Yields decrypted bytes.
49 """
50 for c, key in izip(data, keystream):
51 new_c = (ord(c) ^ key) & 0xff
52 yield chr(new_c)
53
54
55 def decrypt_data(data):
56 """Takes a binary blob uploaded from a game and returns the original binary
57 blob. Depending on your perspective, the returned value may be more
58 intelligible.
59 """
60 # GTS encryption is a simple stream cipher.
61 # The first four bytes of the data are a header containing an obfuscated
62 # key; the rest is the message
63 obf_key_blob, message = data[0:4], data[4:]
64 obf_key, = struct.unpack('>I', obf_key_blob)
65 key = obf_key ^ 0x4a3b2c1d
66
67 # Data is XORed with the output of an LCG, like everything else in Pokémon
68 return ''.join( stream_decipher(message, gts_prng(key)) )
69
70
71 ### Controller!
72
73 def dbg(*args):
74 #print ' '.join(args)
75 pass
76
77 class GTSController(BaseController):
78
79 def dispatch(self, page):
80 """We do our own dispatching for two reasons:
81
82 1. All requests do challenge/response before anything, and it's easier
83 to do that and then dispatch than copy/paste some block of code to
84 every action.
85
86 2. Easier to dump stuff if desired.
87 """
88
89 dbg(request)
90
91 # Always return binary!
92 response.headers['Content-type'] = 'application/octet-stream'
93
94 if 'hash' in request.params:
95 # Okay, already done the response. Decrypt, then dispatch!
96 if request.params['data']:
97 # Note: base64 doesn't like unicode. Go figure. It's binary
98 # junk, anyway.
99 encrypted_data = urlsafe_b64decode(str(request.params['data']))
100 data = decrypt_data(encrypted_data)
101 data = data[4:] # data always starts with pid; we don't care
102 else:
103 data = ''
104
105 method = 'page_' + page
106 if not hasattr(self, method):
107 dbg("NOT YET IMPLEMENTED?")
108 abort(404)
109
110 res = getattr(self, method)(request.params['pid'], data)
111 dbg("RESPONDING WITH ", type(res), len(res), repr(res))
112 return res
113
114 # No hash. Need to issue a challenge. It's random, so whatever
115 return ''.join(sample( uppercase + lowercase + digits, 32 ))
116
117 ### Actual pages
118
119 def page_info(self, pid, data):
120 """info.asp
121
122 Apparently always returns 0x0001. Probably just a ping to see if the
123 server is up.
124 """
125
126 return '\x01\x00'
127
128 def page_setProfile(self, pid, data):
129 """setProfile.asp
130
131 Only Pt and later check this page. Don't know why. It returns eight
132 NULs, every time.
133 """
134
135 return '\x00' * 8
136
137 def page_result(self, pid, data):
138 u"""result.asp
139
140 The good part!
141
142 This checks the game's status on the GTS. If there's nothing
143 interesting to report, it returns 0x0005. If the game has a Pokémon up
144 on the GTS, it returns 0x0004.
145
146 However... if the game has a Pokémon waiting to come to it, it returns
147 that entire Pokémon struct! No header, no encryption. Just a regular
148 ol' Pokémon blob.
149 """
150
151 # Check for an existing Pokémon
152 # TODO support multiple!
153 try:
154 stored_pokemon = meta.Session.query(gts_model.GTSPokemon) \
155 .filter_by(pid=pid) \
156 .one()
157 # We've got one! Cool, send it back. The game will ask us to
158 # delete it after receiving successfully
159 pokemon_save = SaveFilePokemon(stored_pokemon.pokemon_blob)
160 return pokemon_save.as_encrypted
161
162 except:
163 # Nothing
164 return '\x05\x00'
165
166 def page_delete(self, pid, data):
167 u"""delete.asp
168
169 If a Pokémon is received from result.asp, the game requests this page
170 to confirm that the incoming Pokémon may be deleted.
171
172 Returns 0x0001.
173 """
174
175 meta.Session.query(gts_model.GTSPokemon).filter_by(pid=pid).delete()
176 meta.Session.commit()
177
178 return '\x01\x00'
179
180 def page_post(self, pid, data):
181 u"""post.asp
182
183 Deposits a Pokémon in the GTS. Returns 0x0001 on success, or 0x000c if
184 the deposit is rejected.
185 """
186
187 try:
188 # The uploaded Pokémon is encrypted, which is not very useful
189 pokemon_save = SaveFilePokemon(data, encrypted=True)
190
191 # Create a record...
192 stored_pokemon = gts_model.GTSPokemon(
193 pid=pid,
194 pokemon_blob=pokemon_save.as_struct,
195 )
196 meta.Session.add(stored_pokemon)
197 meta.Session.commit()
198 return '\x01\x00'
199 except:
200 # If that failed (presumably due to unique key collision), we're
201 # already storing something. Reject!
202 return '\x0c\x00'
203
204 def page_post_finish(self, pid, data):
205 u"""post_finish.asp
206
207 Surely this does something, but for the life of me I can't figure out
208 what.
209
210 Returns 0x0001.
211 """
212 return '\x01\x00'