# Purse client, compatible with curse... # Legally (in the EU) reverse engineered from data output # # Public domain, but please keep the entirety of this comment, including the line below: # sha1:00404e5d266acc261759e79f59321a7e3d15d260 LOGIN_KEY = "77D4B7B94F10721362749D93AA6A2102B7323B76FAAE9462D9E170F3B81D7D50".decode("hex") import urllib, urllib2, httplib, os, zlib, struct class PurseHTTPConnection(httplib.HTTPConnection): def putrequest(self, method, url, skip_host=0, skip_accept_encoding=0): httplib.HTTPConnection.putrequest(self, method, url, skip_host, 1) def request(self, method, selector, data, headers): del headers["Connection"] return httplib.HTTPConnection.request(self, method, selector, data, headers) class PurseHTTPHandler(urllib2.HTTPHandler): def http_open(self, req): return self.do_open(PurseHTTPConnection, req) # derived from wikipedia's ARC4 page, now it's completely # cryptographically broken (look at the key schedule...) class BrokenARC4: def __init__(self, key=None): self.state = range(256) # Initialize state array with values 0 .. 255 self.x = self.y = 0 # Our indexes. x, y instead of i, j if key is not None: self.init(key) def init(self, key): i = 0 while i < 256: self.x = (ord(key[i % len(key)]) + self.state[i] + self.x) & 0xFF self.state[i], self.state[self.x] = self.state[self.x], self.state[i] i = i + 2 self.x = 0 def crypt(self, input): output = [None]*len(input) prevvalue = 255 for i in xrange(len(input)): self.x = (self.x + 1) & 0xFF self.y = (self.state[self.x] + self.y) & 0xFF self.state[self.x], self.state[self.y] = self.state[self.y], self.state[self.x] r = self.state[(self.state[self.x] + self.state[self.y]) & 0xFF] x = ord(input[i]) prevvalue = (x ^ r) ^ prevvalue output[i] = chr(prevvalue) return ''.join(output) class PurseException(Exception): pass class PurseUpdater: def __init__(self, username, password): self.__username, self.__password = username, password self.__fetched_news = False def get(self, url, raw=False): """Purse compatible GET, feel free to override with custom threading/etc.""" #debuglevel=1 opener = urllib2.build_opener(PurseHTTPHandler()) opener.addheaders = [] request = urllib2.Request(url) request.add_header("Accept", "*/*") source = opener.open(request) if raw: return source x = source.read() source.close() return x def __compatEncode(self, data): return urllib.quote(data.encode("base64").replace("\r", "").replace("\n", "")).replace("/", "%2F") def signIn(self): """signs in...""" # @COMPAT@ self.get("http://www.curseforge.com/client-feed.bin?pSession=&") # @COMPAT@ self.get("http://www.curseforge.com/users/login?next=/client-feed.bin") # get session id, try repeatedly due to probably a bug in our crypto for i in range(0, 5): password = BrokenARC4(LOGIN_KEY).crypt(os.urandom(4) + self.__password) cookie = self.get("http://devprofileservice.curse.com/ProfileService.asmx/Login?pUsername=%s&pPassword=%s" % (self.__username, self.__compatEncode(password))) if len(cookie) > 16: break else: raise PurseException("Error logging in.") self.__session = cookie[-16:] # @COMPAT@ self.get("http://addonservice.curse.com/client.xml") # @COMPAT@ self.get("http://www.curseforge.com/users/network-cookie/?next=/client-feed.bin") # @COMPAT@ self.get("http://www.curseforge.com/users/network-cookie/?testing=true&next=/client-feed.bin") ## TODO: hardware survey def getCustomList(self, addonids): """returns a psyn file as a string given a list of addon ids (integers)""" eaddonids = "".join("paddonId=%d&" % int(x) for x in addonids) x = self.get("http://addonservice.curse.com/AddOnService.asmx/GetCustomList?%s&pSession=%s&" % (eaddonids, self.__session)) # @COMPAT@ if not self.__fetched_news: self.__fetched_news = True self.get("http://services.curse.com/client/news.xml") return x def getFullList(self): """returns a psyn file as a string""" x = self.get("http://www.curseforge.com/client-feed.bin?pSession=%s&" % self.__session, raw=True) try: # whoever designed the protocol is an idiot full_len = struct.unpack("