Updated authentication.py and made it work with the rest of pyCraft

This commit is contained in:
Jeppe Klitgaard 2015-04-01 23:38:10 +02:00
parent edacd24c81
commit 6cf6110bc5
5 changed files with 299 additions and 115 deletions

View File

@ -1,106 +1,277 @@
import urllib2
"""
Handles authentication with the Mojang authentication server.
"""
import requests
import json
from .exceptions import YggdrasilError
import collections
#: The base url for Yggdrasil requests
BASE_URL = 'https://authserver.mojang.com/'
AGENT_INFO = {"name": "Minecraft", "version": 1}
AUTHSERVER = "https://authserver.mojang.com"
SESSIONSERVER = "https://sessionserver.mojang.com/session/minecraft"
# Need this content type, or authserver will complain
CONTENT_TYPE = "application/json"
HEADERS = {"content-type": CONTENT_TYPE}
class YggdrasilError(Exception):
"""Signals some sort of problem while handling a request to
the Yggdrasil service
:ivar human_error: A human readable description of the problem
:ivar error: A short description of the problem
class Profile(object):
"""
def __init__(self, error, human_readable_error):
self.error = error
self.human_readable_error = human_readable_error
def make_request(url, payload):
"""Makes an http request to the Yggdrasil authentication service
If there is an error then it will raise a :exc:`.YggdrasilError`
:param url: The fully formed url to the Yggdrasil endpoint, for example:
https://authserver.mojang.com/authenticate
You may use the :attr:`.BASE_URL` constant here as a shortcut
:param payload: The payload to send with the request, will be interpreted
as a JSON object so be careful.
Example: {"username": username, "password": password, "agent": AGENT_INFO}
:return: A :class:`.Response` object.
Container class for a MineCraft Selected profile.
See: `<http://wiki.vg/Authentication>`_
"""
response = Response()
def __init__(self, id_=None, name=None):
self.id = id_
self.name = name
try:
header = {'Content-Type': 'application/json'}
data = json.dumps(payload)
def to_dict(self):
"""
Returns ``self`` in dictionary-form, which can be serialized by json.
"""
if self:
return {"id": self.id,
"name": self.name}
else:
raise AttributeError("Profile is not yet populated.")
req = urllib2.Request(url, data, header)
opener = urllib2.build_opener()
http_response = opener.open(req, None, 10)
http_response = http_response.read()
def __bool__(self):
bool_state = self.id is not None and self.name is not None
return bool_state
except urllib2.HTTPError, e:
error = e.read()
error = json.loads(error)
response.human_error = error['errorMessage']
response.error = error['error']
raise YggdrasilError(error['error'], error['errorMessage'])
except urllib2.URLError, e:
raise YggdrasilError(e.reason, e.reason)
# ohey, everything didn't end up crashing and burning
if http_response == "":
http_response = "{}"
try:
json_response = json.loads(http_response)
except ValueError, e:
raise YggdrasilError(e.message, "JSON parsing exception on data: " + http_response)
response.payload = json_response
return response
# Python 2 support
def __nonzero__(self):
return self.__bool__()
class Response(object):
"""Class to hold responses from Yggdrasil
:ivar payload: The raw payload returned by Yggdrasil
class AuthenticationToken(object):
"""
payload = None
Represents an authentication token.
class LoginResponse(object):
"""A container class to hold information received from Yggdrasil
upon logging into an account.
:ivar username: The actual in game username of the user
:ivar access_token: The access token of the user, used in place of the password
:ivar profile_id: The selected profile id
See http://wiki.vg/Authentication.
"""
pass
AGENT_NAME = "Minecraft"
AGENT_VERSION = 1
def __init__(self, username=None, access_token=None, client_token=None):
"""
Constructs an `AuthenticationToken` based on `access_token` and
`client_token`.
Parameters:
access_token - An `str` object containing the `access_token`.
client_token - An `str` object containing the `client_token`.
Returns:
A `AuthenticationToken` with `access_token` and `client_token` set.
"""
self.username = username
self.access_token = access_token
self.client_token = client_token
self.profile = Profile()
@property
def authenticated(self):
"""
Attribute which is ``True`` when the token is authenticated and
``False`` when it isn't.
"""
# TODO
return True
def authenticate(self, username, password):
"""
Authenticates the user against https://authserver.mojang.com using
`username` and `password` parameters.
Parameters:
username - An `str` object with the username (unmigrated accounts)
or email address for a Mojang account.
password - An `str` object with the password.
Returns:
Returns `True` if successful.
Otherwise it will raise an exception.
Raises:
minecraft.exceptions.YggdrasilError
"""
payload = {
"agent": {
"name": self.AGENT_NAME,
"version": self.AGENT_VERSION
},
"username": username,
"password": password
}
req = _make_request(AUTHSERVER, "authenticate", payload)
_raise_from_request(req)
json_resp = req.json()
self.username = username
self.access_token = json_resp["accessToken"]
self.client_token = json_resp["clientToken"]
self.profile.id = json_resp["selectedProfile"]["id"]
self.profile.name = json_resp["selectedProfile"]["name"]
return True
def refresh(self):
"""
Refreshes the `AuthenticationToken`. Used to keep a user logged in
between sessions and is preferred over storing a user's password in a
file.
Returns:
Returns `True` if `AuthenticationToken` was successfully refreshed.
Otherwise it raises an exception.
Raises:
minecraft.exceptions.YggdrasilError
ValueError - if `AuthenticationToken.access_token` or
`AuthenticationToken.client_token` isn't set.
"""
if self.access_token is None:
raise ValueError("'access_token' not set!'")
if self.client_token is None:
raise ValueError("'client_token' is not set!")
req = _make_request(AUTHSERVER,
"refresh", {"accessToken": self.access_token,
"clientToken": self.client_token})
_raise_from_request(req)
json_resp = req.json()
self.access_token = json_resp["accessToken"]
self.client_token = json_resp["clientToken"]
self.profile.id = json_resp["selectedProfile"]["id"]
self.profile.name = json_resp["selectedProfile"]["name"]
return True
def validate(self):
"""
Validates the AuthenticationToken.
`AuthenticationToken.access_token` must be set!
Returns:
Returns `True` if `AuthenticationToken` is valid.
Otherwise it will raise an exception.
Raises:
minecraft.exceptions.YggdrasilError
ValueError - if `AuthenticationToken.access_token` is not set.
"""
if self.access_token is None:
raise ValueError("'access_token' not set!")
req = _make_request(AUTHSERVER, "validate",
{"accessToken": self.access_token})
if _raise_from_request(req) is None:
return True
@staticmethod
def sign_out(username, password):
"""
Invalidates `access_token`s using an account's
`username` and `password`.
Parameters:
TODO
Returns:
Returns `True` if sign out was successful.
Otherwise it will raise an exception.
Raises:
minecraft.exceptions.YggdrasilError
"""
req = _make_request(AUTHSERVER, "signout", {"username": username,
"password": password})
if _raise_from_request(req) is None:
return True
def invalidate(self):
"""
Invalidates `access_token`s using the token pair stored in
the `AuthenticationToken`.
Returns:
TODO
Raises:
TODO
"""
pass
def join(self, server_id):
"""
Informs the Mojang session-server that we're joining the
MineCraft server with id ``server_id``.
Parameters:
server_id - ``str`` with the server id
Returns:
``True`` if no errors occured
Raises:
:class:`minecraft.exceptions.YggdrasilError`
"""
req = _make_request(SESSIONSERVER, "join",
{"accessToken": self.access_token,
"selectedProfile": self.profile.to_dict(),
"serverId": server_id})
if req.status_code == requests.codes.ok:
return True
else:
return req
err = "Failed to join game. Status code: {}"
raise YggdrasilError(err.format(str(req.status_code)))
def login_to_minecraft(username, password):
def _make_request(server, endpoint, data):
"""
Logs in to a minecraft account
Will raise a :exc:`.YggdrasilError` on failure
Fires a POST with json-packed data to the given endpoint and returns
response.
:param username: The mojang account username
:param password: The password for the account
:return: A :class:`.LoginResponse` object
Parameters:
endpoint - An `str` object with the endpoint, e.g. "authenticate"
data - A `dict` containing the payload data.
Returns:
A `requests.Request` object.
"""
payload = {"username": username, "password": password, "agent": AGENT_INFO}
response = make_request(BASE_URL + "authenticate", payload)
req = requests.post(server + "/" + endpoint, data=json.dumps(data),
headers=HEADERS)
return req
login_response = LoginResponse()
payload = response.payload
login_response.access_token = payload["accessToken"]
login_response.profile_id = payload["selectedProfile"]["id"]
login_response.username = payload["selectedProfile"]["name"]
def _raise_from_request(req):
"""
Raises an appropriate `YggdrasilError` based on the `status_code` and
`json` of a `requests.Request` object.
"""
if req.status_code == requests.codes.ok:
return None
return login_response
json_resp = req.json()
if "error" not in json_resp and "errorMessage" not in json_resp:
raise YggdrasilError("Malformed error message.")
message = "[{status_code}] {error}: '{error_message}'"
message = message.format(status_code=str(req.status_code),
error=json_resp["error"],
error_message=json_resp["errorMessage"])
raise YggdrasilError(message)

9
minecraft/exceptions.py Normal file
View File

@ -0,0 +1,9 @@
"""
Contains the `Exceptions` used by this library.
"""
class YggdrasilError(Exception):
"""
Base `Exception` for the Yggdrasil authentication service.
"""

View File

@ -39,17 +39,17 @@ class Connection(object):
#: Indicates if this connection is spawned in the Minecraft game world
spawned = False
def __init__(self, address, port, login_response):
def __init__(self, address, port, auth_token):
"""Sets up an instance of this object to be able to connect to a minecraft server.
The connect method needs to be called in order to actually begin the connection
:param address: address of the server to connect to
:param port(int): port of the server to connect to
:param login_response: :class:`authentication.LoginResponse` object as obtained from the authentication package
:param auth_token: :class:`authentication.AuthenticationToken` object.
"""
self.options.address = address
self.options.port = port
self.login_response = login_response
self.auth_token = auth_token
self.reactor = PacketReactor(self)
def _start_network_thread(self):
@ -120,7 +120,7 @@ class Connection(object):
self.reactor = LoginReactor(self)
self._start_network_thread()
login_start_packet = LoginStartPacket()
login_start_packet.name = self.login_response.username
login_start_packet.name = self.auth_token.username
self.write_packet(login_start_packet)
def _connect(self):
@ -246,14 +246,10 @@ class LoginReactor(PacketReactor):
# A server id of '-' means the server is in offline mode
if packet.server_id != '-':
url = "https://sessionserver.mojang.com/session/minecraft/join"
server_id = encryption.generate_verification_hash(packet.server_id, secret, packet.public_key)
payload = {'accessToken': self.connection.login_response.access_token,
'selectedProfile': self.connection.login_response.profile_id,
'serverId': server_id}
authentication.make_request(url, payload)
self.connection.auth_token.join(server_id)
encryption_response = EncryptionResponsePacket()
encryption_response.shared_secret = encrypted_secret
encryption_response.verify_token = encrypted_token

View File

@ -1 +1,2 @@
cryptography
requests

View File

@ -2,12 +2,15 @@ import getpass
import sys
from optparse import OptionParser
from pprint import pprint
from minecraft import authentication
from minecraft.exceptions import YggdrasilError
from minecraft.networking.connection import Connection
from minecraft.networking.packets import ChatMessagePacket, ChatPacket
def main():
def get_options():
parser = OptionParser()
parser.add_option("-u", "--username", dest="username", default=None,
@ -27,34 +30,38 @@ def main():
if not options.password:
options.password = getpass.getpass("Enter your password: ")
try:
login_response = authentication.login_to_minecraft(options.username, options.password)
from pprint import pprint # TODO: remove debug
pprint(vars(login_response)) # TODO: remove debug
except authentication.YggdrasilError as e:
print e.human_readable_error
return
print("Logged in as " + login_response.username)
if not options.server:
options.server = raw_input("Please enter server address (including port): ")
# Try to split out port and address
if ':' in options.server:
server = options.server.split(":")
address = server[0]
port = int(server[1])
options.address = server[0]
options.port = int(server[1])
else:
address = options.server
port = 25565
options.address = options.server
options.port = 25565
connection = Connection(address, port, login_response)
return options
def main():
options = get_options()
auth_token = authentication.AuthenticationToken()
try:
auth_token.authenticate(options.username, options.password)
except YggdrasilError as e:
print(e.error)
sys.exit()
print("Logged in as " + auth_token.username)
connection = Connection(options.address, options.port, auth_token)
connection.connect()
def print_chat(chat_packet):
print "Position: " + str(chat_packet.position)
print "Data: " + chat_packet.json_data
print("Position: " + str(chat_packet.position))
print("Data: " + chat_packet.json_data)
connection.register_packet_listener(print_chat, ChatMessagePacket)
while True:
@ -64,7 +71,7 @@ def main():
packet.message = text
connection.write_packet(packet)
except KeyboardInterrupt:
print "Bye!"
print("Bye!")
sys.exit()