mirror of
https://github.com/ammaraskar/pyCraft.git
synced 2024-11-25 03:35:29 +01:00
Updated authentication.py and made it work with the rest of pyCraft
This commit is contained in:
parent
edacd24c81
commit
6cf6110bc5
@ -1,106 +1,277 @@
|
|||||||
import urllib2
|
"""
|
||||||
|
Handles authentication with the Mojang authentication server.
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
import json
|
import json
|
||||||
|
from .exceptions import YggdrasilError
|
||||||
|
import collections
|
||||||
|
|
||||||
#: The base url for Yggdrasil requests
|
AUTHSERVER = "https://authserver.mojang.com"
|
||||||
BASE_URL = 'https://authserver.mojang.com/'
|
SESSIONSERVER = "https://sessionserver.mojang.com/session/minecraft"
|
||||||
AGENT_INFO = {"name": "Minecraft", "version": 1}
|
# Need this content type, or authserver will complain
|
||||||
|
CONTENT_TYPE = "application/json"
|
||||||
|
HEADERS = {"content-type": CONTENT_TYPE}
|
||||||
|
|
||||||
|
|
||||||
class YggdrasilError(Exception):
|
class Profile(object):
|
||||||
"""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
|
|
||||||
"""
|
"""
|
||||||
|
Container class for a MineCraft Selected profile.
|
||||||
def __init__(self, error, human_readable_error):
|
See: `<http://wiki.vg/Authentication>`_
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
response = Response()
|
def __init__(self, id_=None, name=None):
|
||||||
|
self.id = id_
|
||||||
|
self.name = name
|
||||||
|
|
||||||
try:
|
def to_dict(self):
|
||||||
header = {'Content-Type': 'application/json'}
|
"""
|
||||||
data = json.dumps(payload)
|
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)
|
def __bool__(self):
|
||||||
opener = urllib2.build_opener()
|
bool_state = self.id is not None and self.name is not None
|
||||||
http_response = opener.open(req, None, 10)
|
return bool_state
|
||||||
http_response = http_response.read()
|
|
||||||
|
|
||||||
except urllib2.HTTPError, e:
|
# Python 2 support
|
||||||
error = e.read()
|
def __nonzero__(self):
|
||||||
error = json.loads(error)
|
return self.__bool__()
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class Response(object):
|
class AuthenticationToken(object):
|
||||||
"""Class to hold responses from Yggdrasil
|
|
||||||
|
|
||||||
:ivar payload: The raw payload returned by Yggdrasil
|
|
||||||
"""
|
"""
|
||||||
payload = None
|
Represents an authentication token.
|
||||||
|
|
||||||
|
See http://wiki.vg/Authentication.
|
||||||
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
|
|
||||||
"""
|
"""
|
||||||
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 login_to_minecraft(username, password):
|
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 _make_request(server, endpoint, data):
|
||||||
"""
|
"""
|
||||||
Logs in to a minecraft account
|
Fires a POST with json-packed data to the given endpoint and returns
|
||||||
Will raise a :exc:`.YggdrasilError` on failure
|
response.
|
||||||
|
|
||||||
:param username: The mojang account username
|
Parameters:
|
||||||
:param password: The password for the account
|
endpoint - An `str` object with the endpoint, e.g. "authenticate"
|
||||||
:return: A :class:`.LoginResponse` object
|
data - A `dict` containing the payload data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A `requests.Request` object.
|
||||||
"""
|
"""
|
||||||
payload = {"username": username, "password": password, "agent": AGENT_INFO}
|
req = requests.post(server + "/" + endpoint, data=json.dumps(data),
|
||||||
response = make_request(BASE_URL + "authenticate", payload)
|
headers=HEADERS)
|
||||||
|
return req
|
||||||
|
|
||||||
login_response = LoginResponse()
|
|
||||||
payload = response.payload
|
|
||||||
|
|
||||||
login_response.access_token = payload["accessToken"]
|
def _raise_from_request(req):
|
||||||
login_response.profile_id = payload["selectedProfile"]["id"]
|
"""
|
||||||
login_response.username = payload["selectedProfile"]["name"]
|
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
9
minecraft/exceptions.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
"""
|
||||||
|
Contains the `Exceptions` used by this library.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class YggdrasilError(Exception):
|
||||||
|
"""
|
||||||
|
Base `Exception` for the Yggdrasil authentication service.
|
||||||
|
"""
|
@ -39,17 +39,17 @@ class Connection(object):
|
|||||||
#: Indicates if this connection is spawned in the Minecraft game world
|
#: Indicates if this connection is spawned in the Minecraft game world
|
||||||
spawned = False
|
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.
|
"""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
|
The connect method needs to be called in order to actually begin the connection
|
||||||
|
|
||||||
:param address: address of the server to connect to
|
:param address: address of the server to connect to
|
||||||
:param port(int): port 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.address = address
|
||||||
self.options.port = port
|
self.options.port = port
|
||||||
self.login_response = login_response
|
self.auth_token = auth_token
|
||||||
self.reactor = PacketReactor(self)
|
self.reactor = PacketReactor(self)
|
||||||
|
|
||||||
def _start_network_thread(self):
|
def _start_network_thread(self):
|
||||||
@ -120,7 +120,7 @@ class Connection(object):
|
|||||||
self.reactor = LoginReactor(self)
|
self.reactor = LoginReactor(self)
|
||||||
self._start_network_thread()
|
self._start_network_thread()
|
||||||
login_start_packet = LoginStartPacket()
|
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)
|
self.write_packet(login_start_packet)
|
||||||
|
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
@ -246,13 +246,9 @@ class LoginReactor(PacketReactor):
|
|||||||
|
|
||||||
# A server id of '-' means the server is in offline mode
|
# A server id of '-' means the server is in offline mode
|
||||||
if packet.server_id != '-':
|
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)
|
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 = EncryptionResponsePacket()
|
||||||
encryption_response.shared_secret = encrypted_secret
|
encryption_response.shared_secret = encrypted_secret
|
||||||
|
@ -1 +1,2 @@
|
|||||||
cryptography
|
cryptography
|
||||||
|
requests
|
||||||
|
47
start.py
47
start.py
@ -2,12 +2,15 @@ import getpass
|
|||||||
import sys
|
import sys
|
||||||
from optparse import OptionParser
|
from optparse import OptionParser
|
||||||
|
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
from minecraft import authentication
|
from minecraft import authentication
|
||||||
|
from minecraft.exceptions import YggdrasilError
|
||||||
from minecraft.networking.connection import Connection
|
from minecraft.networking.connection import Connection
|
||||||
from minecraft.networking.packets import ChatMessagePacket, ChatPacket
|
from minecraft.networking.packets import ChatMessagePacket, ChatPacket
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def get_options():
|
||||||
parser = OptionParser()
|
parser = OptionParser()
|
||||||
|
|
||||||
parser.add_option("-u", "--username", dest="username", default=None,
|
parser.add_option("-u", "--username", dest="username", default=None,
|
||||||
@ -27,34 +30,38 @@ def main():
|
|||||||
if not options.password:
|
if not options.password:
|
||||||
options.password = getpass.getpass("Enter your 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:
|
if not options.server:
|
||||||
options.server = raw_input("Please enter server address (including port): ")
|
options.server = raw_input("Please enter server address (including port): ")
|
||||||
# Try to split out port and address
|
# Try to split out port and address
|
||||||
if ':' in options.server:
|
if ':' in options.server:
|
||||||
server = options.server.split(":")
|
server = options.server.split(":")
|
||||||
address = server[0]
|
options.address = server[0]
|
||||||
port = int(server[1])
|
options.port = int(server[1])
|
||||||
else:
|
else:
|
||||||
address = options.server
|
options.address = options.server
|
||||||
port = 25565
|
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()
|
connection.connect()
|
||||||
|
|
||||||
def print_chat(chat_packet):
|
def print_chat(chat_packet):
|
||||||
print "Position: " + str(chat_packet.position)
|
print("Position: " + str(chat_packet.position))
|
||||||
print "Data: " + chat_packet.json_data
|
print("Data: " + chat_packet.json_data)
|
||||||
|
|
||||||
connection.register_packet_listener(print_chat, ChatMessagePacket)
|
connection.register_packet_listener(print_chat, ChatMessagePacket)
|
||||||
while True:
|
while True:
|
||||||
@ -64,7 +71,7 @@ def main():
|
|||||||
packet.message = text
|
packet.message = text
|
||||||
connection.write_packet(packet)
|
connection.write_packet(packet)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print "Bye!"
|
print("Bye!")
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user