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