Source code for dplib.dplogin

# DPLib - Asynchronous bot framework for Digital Paint: Paintball 2 servers
# Copyright (C) 2017  Michał Rokita
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
DPLogin - a module for managing DPLogin accounts.
"""
from hashlib import md5
from urllib.parse import urlencode
from urllib.request import build_opener, HTTPCookieProcessor
import re
from http.cookiejar import Cookie, CookieJar

PATTERN_USER_ID = re.compile("User ID: (\d+)")
PATTERN_CLANS = re.compile("/index.php\\?action=viewclan&clanid=(\d+)\\\">"
                           "(.*?) \\- (.*?)</a>")
PATTERN_PROFILE_DATA = re.compile("name=\\\"(.*?)\\\" .*?value=\\\"(.*?)\\\"")
PATTERN_PROFILE_BIO = re.compile("name=\\\"(bio)\\\" wrap=soft>"
                                 "(.*?)</textarea>", re.DOTALL)
PATTERN_MEMBERS = re.compile("(?:<b class=\\\"faqtitle\\\">"
                             "(Leaders|Former Members|Invited Players Pending"
                             "|Players Requesting Membership):"
                             "</b></td></tr>)?<tr><td><a href=\\\"/index\\.php"
                             "\\?action=viewmember&playerid=(\d+)\\\""
                             ">([^<>]+)</a></td><td>.*?</td></tr>")


[docs]def get_session_hash(pwhash, session_id): """ Hash password again, use session_id as a seed. Used at log in when no plaintext password is specified :param pwhash: :param session_id: :return: Session hash :rtype str: """ return hex_md5(pwhash+session_id)
[docs]def get_password_hash(password, user_id, session_id): """ Hash plain password a few times. Used at log in when a plaintext password is specified. :param password: :param user_id: :param session_id: :return: Hashed password :rtype: str """ return hex_md5(hex_md5(hex_md5(password + "DPLogin001") + user_id) + session_id)
[docs]def get_new_password_hash(password, user_id): """ Hash the password at password change. :param password: :param user_id: :return: Hashed password and user id :rtype: str """ return hex_md5(hex_md5(password + "DPLogin001") + user_id)
[docs]def hex_md5(string): """ Hash a string using md5 :param string: String to hash :return: MD5 hash of string :rtype: str """ return md5(string.encode('utf-8')).hexdigest()
[docs]class DPLogin: """ A class that represents a DPLogin session. :param username: :param password: :param pw_hash: :param pw_session_hash: :param session_id: :raises TypeError: Wrong password or hash """ def __init__(self, username=None, password=None, pw_hash=None, pw_session_hash=None, session_id=None): if not ((username and password) or session_id): raise TypeError("not enough parameters") self.__cj = CookieJar() self.__opener = build_opener( HTTPCookieProcessor(self.__cj)) self.__opener.addheaders = [ ('User-Agent', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100102' ' Firefox/46.0)') ] self.username = username self.password = password self.pw_hash = pw_hash self.pw_session_hash = pw_session_hash self.session_id = None if not username and not (password or pw_hash or pw_session_hash) and session_id: self.set_sessid(session_id) return response = self.__opener.open("http://dplogin.com/index.php", data=urlencode({"action": "weblogin1", "username": username, "pwhash": ""}).encode('utf-8')).read().decode('utf-8') try: self.user_id = PATTERN_USER_ID.findall(response)[0] except IndexError: raise TypeError("User {} doesn't exist.".format(username)) self.session_id = None if not session_id: for cookie in self.__cj: if cookie.name == "PHPSESSID": self.set_sessid(cookie.value) else: self.set_sessid(session_id) pwhash = None if password: pwhash = get_password_hash(password, self.user_id, self.session_id) elif pw_hash: pwhash = get_session_hash(pw_hash, session_id=session_id) elif pw_session_hash: pwhash = pw_session_hash response = self.__opener.open("http://dplogin.com/index.php", data=urlencode({"action": "weblogin2", "username": username, "pwhash": pwhash, "password": ""}).encode('utf-8')).read().decode('utf-8') if "Invalid password" in response: raise TypeError("Wrong password.")
[docs] def set_sessid(self, session_id): """ Set session id to session_id. Used to "fake" session_id, pretty useful for hijacking ;) :param session_id: session id to set :return: None :rtype: NoneType """ self.session_id = session_id self.__cj.set_cookie(Cookie(name="PHPSESSID", value=session_id, port=None, port_specified=False, domain='dplogin.com', domain_specified=False, domain_initial_dot=False, path='/', secure=False, expires=None, discard=True, comment=None, rest={'HttpOnly': None}, rfc2109=False, comment_url=None, path_specified=False, version=0))
[docs] def get_clans(self): """ Get clans of the user. :return: A list of dicts with clan info :rtype: list """ response = self.__opener.open("http://dplogin.com/index.php?" "action=main").read().decode('utf-8') data = PATTERN_CLANS.findall(response) has_active_clan = len(data) and ">Active Clan<" in response clans = [] i = 0 for clan in data: clans.append({"id": clan[0], "name": clan[1], "tag": [2], "active": has_active_clan and not i}) if not i: i = 1 return clans
[docs] def leave_current_clan(self): """ Leave the current clan. :return: HTTP Response :rtype: str """ for clan in self.get_clans(): if clan["active"]: return self.leave_clan(clan["id"])
[docs] def leave_clan(self, clan_id): """ Leave clan with clan_id. :param clan_id: id of the clan to leave :return: HTTP Response :rtype: str """ return self.__opener.open("http://dplogin.com/index.php?" "action=leaveclan&clanid={}" .format(clan_id)).read().decode('utf-8')
[docs] def join_clan(self, clan_id=None, clan_name=None): """ Join a clan. :param clan_id: id of a clan :param clan_name: name of a clan :return: HTTP Response :rtype: str """ if not (clan_id or clan_name): raise TypeError("Not enough parameters") if clan_id: return self.__opener.open("http://dplogin.com/index.php?action=" "joinclan&clanid={}". format(clan_id)).read().decode('utf-8') else: return self.__opener.open("http://dplogin.com/index.php", urlencode( {"action": "joinclan", "clanname": "clan_name"}).encode('utf-8')).read().decode('utf-8')
[docs] def get_profile_data(self): """ Get profile data. :return: dict({"field": "value"}) :rtype: dict """ response = self.__opener.open("http://dplogin.com/index.php?" "action=editprofile").read().decode('utf-8') data = PATTERN_PROFILE_DATA.findall(response) data.extend(PATTERN_PROFILE_BIO.findall(response)) return dict([tuple(i) for i in data])
[docs] def update_profile(self, newpassword=None, email=None, realname=None, birthdate=None, location=None, displayemail=None, forumname=None, aim=None, icq=None, msn=None, yim=None, website=None, bio=None): """ Update DPLogin profile. :param newpassword: :param email: :param realname: :param birthdate: :param location: :param displayemail: :param forumname: :param aim: :param icq: :param msn: :param yim: :param website: :param bio: :return: HTTP response :rtype: str """ form_data = self.get_profile_data() if self.password: form_data["pwhash"] = get_password_hash(self.password, self.user_id, self.session_id) elif self.pw_hash: form_data["pwhash"] = get_session_hash(self.pw_hash, self.session_id) elif self.pw_session_hash: form_data["pwhash"] = self.pw_session_hash else: raise TypeError("A hash/password is required to use this function") form_data["password"] = "" l = locals().copy() del l["form_data"] del l["self"] for var in l: if l[var]: form_data[var] = l[var] if newpassword: form_data["newpwhash"] = get_new_password_hash(newpassword, self.user_id) form_data["newpassword"] = "" form_data["newpassword2"] = "" return self.__opener.open("http://dplogin.com/index.php", urlencode(form_data).encode('utf-8')).read().decode('utf-8')
[docs] def del_name(self, name): """ Delete a name from the account. :param name: name to be deleted :return: HTTP response :rtype: str """ return self.__opener.open("http://dplogin.com/index.php?" "action=deletemyname&name={}" .format(name)).read().decode('utf-8')
[docs] def add_name(self, name): """ Add a name to the account. :param name: name to be deleted :return: HTTP response :rtype: str """ return self.__opener.open("http://dplogin.com/index.php", urlencode({"action": "addnewname", "newname": name}).encode('utf-8')).read().decode('utf-8')
[docs] def create_clan(self, name, tag): """ Create a new clan. :param name: Name of the new clan :param tag: Tag of the new clan :return: HTTP response :rtype: str """ return self.__opener.open("http://dplogin.com/index.php", urlencode({"action": "createclan2", "clanname": name, "clantag": tag}).encode('utf-8')).read().decode('utf-8')
[docs] def invite_member(self, clanid, playerid=None, name=None): """ Invite a member to a clan. :param clanid: id of the clan :param playerid: id of the player to invite :param name: name of the player to invite :return: HTTP response :rtype: str """ if not (clanid or playerid): raise TypeError("Not enough parameters") if name: return self.__opener.open("http://dplogin.com/index.php", urlencode({"action": "inviteclanmember", "clanid": clanid, "playername": name}).encode('utf-8')).read().decode('utf-8') else: return self.__opener.open("http://dplogin.com/index.php?" "action=inviteclanmember" "&clanid={}&playerid={}" .format(clanid, playerid)).read()
[docs] def cancel_join_request(self, clanid): """ Cancel a clan join request. :param clanid: id of a clan :return: HTTP response :rtype: str """ return self.__opener.open("http://dplogin.com/index.php?" "action=cancelclanjoinrequest&clanid={}" .format(clanid)).read().decode('utf-8')
[docs] def reject_join_request(self, clanid, playerid): """ Reject a clan join request. :param clanid: id of a clan :param playerid: id of a player :return: HTTP response :rtype: str """ return self.__opener.open("http://dplogin.com/index.php?" "action=rejectclanjoinrequest" "&clanid={}&playerid={}" .format(clanid, playerid)).read().decode('utf-8')
[docs] def make_leader(self, clanid, playerid): """ Make player with id of playerid a leader of a clan with clanid. :param clanid: id of a clan :param playerid: id of a player :return: HTTP response :rtype: str """ return self.__opener.open("http://dplogin.com/index.php?action=" "makeclanleader&clanid={}&playerid={}" .format(clanid, playerid)).read().decode('utf-8')
[docs] def kick_from_clan(self, clanid, playerid): """ Kick a player with id of playerid from a clan with id of clanid. :param clanid: id of a clan :param playerid: id of a player :return: HTTP response :rtype: str """ return self.__opener.open("http://dplogin.com/index.php?" "action=kickclanmember" "&clanid={}&playerid={}" .format(clanid, playerid)).read().decode('utf-8')
[docs] def remove_clan_leader(self, clanid, playerid): """ Remove a leader with id of playerid from a clan with id of clanid. :param clanid: id of a clan :param playerid: id of a player :return: HTTP response :rtype: str """ return self.__opener.open("http://dplogin.com/index.php?" "action=removeclanleader" "&clanid={}&playerid={}" .format(clanid, playerid)).read().decode('utf-8')
[docs] def cancel_invite(self, clanid, playerid): """ Cancel invite to clan for some player. :param clanid: id of a clan :param playerid: id of a player :return: HTTP response :rtype: str """ return self.__opener.open("http://dplogin.com/index.php?" "action=cancelinviteclanmember" "&clanid={}&playerid={}" .format(clanid, playerid)).read().decode('utf-8')
[docs] def get_clan_members(self, clanid): """ Get members of a clan with id of clanid. :param clanid: id of the clan :return: A list of dict objects with ids, names and ranks. :rtype: list """ data = PATTERN_MEMBERS.findall( self.__opener.open("http://dplogin.com/index.php" "?action=viewclan&clanid={}" .format(clanid)).read().decode('utf-8')) members = {} current_key = "" for member in data: if member[0]: current_key = member[0] members[current_key] = list() members[current_key].append({"id": member[1], "name": member[2], "rank": current_key}) return members