mirror of
https://github.com/ammaraskar/pyCraft.git
synced 2024-12-22 08:38:08 +01:00
Significant headway on the rewrite.
Handle crypto with the excellent crytography library, still a lot of TODOs to take care of but at least we can connect now
This commit is contained in:
parent
f68206140b
commit
7e8df47352
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,3 +1,6 @@
|
||||
*.pyc
|
||||
build/*
|
||||
src/*
|
||||
src/*
|
||||
|
||||
*.iml
|
||||
.idea/
|
@ -12,13 +12,18 @@ class Response(object):
|
||||
payload = None
|
||||
|
||||
|
||||
class YggdrasilError(Exception):
|
||||
|
||||
def __init__(self, error='', human_readable_error=''):
|
||||
self.error = error
|
||||
self.human_readable_error = human_readable_error
|
||||
|
||||
|
||||
def make_request(url, payload):
|
||||
"""Makes http requests to the Yggdrasil authentication service
|
||||
|
||||
Returns a Response object with an error boolean, if there is an error
|
||||
then it will also contain `error` and `human_error` fields
|
||||
otherwise a `payload` field will be returned with the actual response
|
||||
from Yggdrasil
|
||||
Returns a Response object.
|
||||
If there is an error then it will raise a YggdrasilError.
|
||||
"""
|
||||
response = Response()
|
||||
|
||||
@ -35,18 +40,21 @@ def make_request(url, payload):
|
||||
error = e.read()
|
||||
error = json.loads(error)
|
||||
|
||||
response.error = True
|
||||
response.human_error = error['errorMessage']
|
||||
response.error = error['error']
|
||||
return response
|
||||
raise YggdrasilError(error['error'], error['errorMessage'])
|
||||
|
||||
except urllib2.URLError, e:
|
||||
response.error = True
|
||||
response.human_error = e.reason
|
||||
return response
|
||||
raise YggdrasilError(e.reason, e.reason)
|
||||
|
||||
# ohey, everything didn't end up crashing and burning
|
||||
json_response = json.loads(http_response)
|
||||
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
|
||||
|
||||
@ -69,15 +77,11 @@ def login_to_minecraft(username, password):
|
||||
response = make_request(BASE_URL + "authenticate", payload)
|
||||
|
||||
login_response = LoginResponse()
|
||||
if response.error:
|
||||
login_response.error = True
|
||||
login_response.human_error = response.human_error
|
||||
else:
|
||||
payload = response.payload
|
||||
payload = response.payload
|
||||
|
||||
login_response.error = False
|
||||
login_response.access_token = payload["accessToken"]
|
||||
login_response.profile_id = payload["selectedProfile"]["id"]
|
||||
login_response.username = payload["selectedProfile"]["name"]
|
||||
login_response.error = False
|
||||
login_response.access_token = payload["accessToken"]
|
||||
login_response.profile_id = payload["selectedProfile"]["id"]
|
||||
login_response.username = payload["selectedProfile"]["name"]
|
||||
|
||||
return login_response
|
||||
|
@ -1,52 +1,90 @@
|
||||
from collections import deque
|
||||
from threading import Lock
|
||||
from zlib import decompress
|
||||
import threading
|
||||
import socket
|
||||
import time
|
||||
import select
|
||||
import authentication
|
||||
|
||||
from packets import *
|
||||
from start import PROTOCOL_VERSION
|
||||
from types import VarInt
|
||||
|
||||
PROTOCOL_VERSION = 47
|
||||
|
||||
class Connection:
|
||||
|
||||
class ConnectionOptions(object):
|
||||
# TODO: allow these options to be overriden from a constructor below
|
||||
address = None
|
||||
port = None
|
||||
use_encryption = True
|
||||
compression_threshold = -1
|
||||
compression_enabled = False
|
||||
|
||||
|
||||
class Connection(object):
|
||||
"""This class represents a connection to a minecraft
|
||||
server, it handles everything from connecting, sending packets,
|
||||
server, it handles everything from connecting, sending packets to
|
||||
handling default network behaviour
|
||||
"""
|
||||
outgoing_packet_queue = deque()
|
||||
write_lock = Lock()
|
||||
_outgoing_packet_queue = deque()
|
||||
_write_lock = Lock()
|
||||
networking_thread = None
|
||||
options = ConnectionOptions()
|
||||
|
||||
def __init__(self, address, port, login_response):
|
||||
self.address = address
|
||||
self.port = port
|
||||
"""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: login_response object as obtained from the authentication package
|
||||
"""
|
||||
self.options.address = address
|
||||
self.options.port = port
|
||||
self.login_response = login_response
|
||||
self.reactor = HandshakeReactor(self)
|
||||
self.reactor = PacketReactor(self)
|
||||
|
||||
def _start_network_thread(self):
|
||||
self.networking_thread = NetworkingThread(self)
|
||||
self.networking_thread.start()
|
||||
|
||||
def write_packet(self, packet, force=False):
|
||||
if force:
|
||||
self.write_lock.acquire()
|
||||
packet.write(self.socket)
|
||||
self.write_lock.release()
|
||||
else:
|
||||
self.outgoing_packet_queue.append(packet)
|
||||
"""Writes a packet to the server.
|
||||
If force is set to true, the method attempts to acquire the write lock and write the packet
|
||||
out immediately, and as such may block.
|
||||
If force is false then the packet will be added to the end of the packet writing queue
|
||||
to be sent 'as soon as possible'
|
||||
|
||||
:param packet:
|
||||
:param force(bool): Specifies if the packet write should be immediate
|
||||
:return:
|
||||
"""
|
||||
if force:
|
||||
self._write_lock.acquire()
|
||||
if self.options.compression_enabled:
|
||||
packet.write(self.socket, self.options.compression_threshold)
|
||||
else:
|
||||
packet.write(self.socket)
|
||||
self._write_lock.release()
|
||||
else:
|
||||
self._outgoing_packet_queue.append(packet)
|
||||
|
||||
# Mostly a convenience function, caller should make sure they have the
|
||||
# write lock acquired to avoid issues caused by asynchronous access to the socket.
|
||||
# This should be the only method that removes elements from the outbound queue
|
||||
def _pop_packet(self):
|
||||
if len(self.outgoing_packet_queue) == 0:
|
||||
# Pops the topmost packet off the outgoing queue and writes it out through the socket
|
||||
#
|
||||
# Mostly an internal convenience function, caller should make sure they have the
|
||||
# write lock acquired to avoid issues caused by asynchronous access to the socket.
|
||||
# This should be the only method that removes elements from the outbound queue
|
||||
if len(self._outgoing_packet_queue) == 0:
|
||||
return False
|
||||
else:
|
||||
packet = self.outgoing_packet_queue.popleft()
|
||||
print "Writing out: " + hex(packet.id) + " / " + packet.name
|
||||
packet.write(self.socket)
|
||||
packet = self._outgoing_packet_queue.popleft()
|
||||
print "Writing out: " + hex(packet.id) + " / " + packet.packet_name
|
||||
if self.options.compression_enabled:
|
||||
packet.write(self.socket, self.options.compression_threshold)
|
||||
else:
|
||||
packet.write(self.socket)
|
||||
return True
|
||||
|
||||
def status(self):
|
||||
@ -59,23 +97,31 @@ class Connection:
|
||||
self.write_packet(request_packet)
|
||||
|
||||
def connect(self):
|
||||
"""Attempt to begin connecting to the server
|
||||
"""
|
||||
self._connect()
|
||||
self._handshake()
|
||||
|
||||
self.reactor = LoginReactor(self)
|
||||
self._start_network_thread()
|
||||
login_start_packet = LoginStartPacket()
|
||||
login_start_packet.name = self.login_response.username
|
||||
self.write_packet(login_start_packet)
|
||||
|
||||
def _connect(self):
|
||||
# Connect a socket to the server and create a file object from the socket
|
||||
#The file object is used to read any and all data from the socket since it's "guaranteed"
|
||||
#to read the number of bytes specified, the socket itself will mostly be
|
||||
#used to write data upstream to the server
|
||||
# The file object is used to read any and all data from the socket since it's "guaranteed"
|
||||
# to read the number of bytes specified, the socket itself will mostly be
|
||||
# used to write data upstream to the server
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.connect((self.address, self.port))
|
||||
self.socket.connect((self.options.address, self.options.port))
|
||||
self.file_object = self.socket.makefile()
|
||||
|
||||
def _handshake(self, next_state=2):
|
||||
handshake = HandShakePacket()
|
||||
handshake.protocol_version = PROTOCOL_VERSION
|
||||
handshake.server_address = self.address
|
||||
handshake.server_port = self.port
|
||||
handshake.server_address = self.options.address
|
||||
handshake.server_port = self.options.port
|
||||
handshake.next_state = next_state
|
||||
|
||||
self.write_packet(handshake)
|
||||
@ -96,15 +142,12 @@ class NetworkingThread(threading.Thread):
|
||||
break
|
||||
# Attempt to write out as many as 300 packets as possible every 0.05 seconds (20 ticks per second)
|
||||
num_packets = 0
|
||||
self.connection.write_lock.acquire()
|
||||
self.connection._write_lock.acquire()
|
||||
while self.connection._pop_packet():
|
||||
|
||||
self.connection._pop_packet()
|
||||
|
||||
num_packets += 1
|
||||
if num_packets >= 300:
|
||||
break
|
||||
self.connection.write_lock.release()
|
||||
self.connection._write_lock.release()
|
||||
|
||||
# Read and react to as many as 50 packets
|
||||
num_packets = 0
|
||||
@ -121,7 +164,7 @@ class NetworkingThread(threading.Thread):
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
class PacketReactor:
|
||||
class PacketReactor(object):
|
||||
state_name = None
|
||||
clientbound_packets = None
|
||||
|
||||
@ -130,28 +173,122 @@ class PacketReactor:
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
|
||||
# Design objectives:
|
||||
#
|
||||
# Avoid buffering data as much as possible
|
||||
# Wherever possible, read directly from the network stream
|
||||
# Minimize any overheads, reading packets should be simple
|
||||
def read_packet(self, stream):
|
||||
ready = select.select([self.connection.socket], [], [], self.TIME_OUT)
|
||||
if self.connection.socket in ready[0]:
|
||||
length = VarInt.read_socket(self.connection.socket)
|
||||
packet_id = VarInt.read(stream)
|
||||
ready_to_read, _, __ = select.select([self.connection.socket], [], [], self.TIME_OUT)
|
||||
real_stream = stream
|
||||
|
||||
if self.connection.socket in ready_to_read:
|
||||
length = VarInt.read_socket(self.connection.socket)
|
||||
|
||||
if self.connection.options.compression_enabled:
|
||||
compressed_size = VarInt.read(stream)
|
||||
# If this is a compressed packet we'll need to decompress it into a buffer
|
||||
# and then pretend that that is the actual network stream
|
||||
if compressed_size > 0:
|
||||
compressed_packet = stream.read(compressed_size)
|
||||
stream = PacketBuffer()
|
||||
stream.send(decompress(compressed_packet))
|
||||
stream.reset_cursor()
|
||||
|
||||
packet_id = VarInt.read_socket(self.connection.socket)
|
||||
|
||||
print "Reading packet: " + str(packet_id) + " / " + hex(packet_id) + " (Size: " + str(length) + ")"
|
||||
|
||||
# If we know the structure of the packet, attempt to parse it
|
||||
# otherwise just skip it
|
||||
if packet_id in self.clientbound_packets:
|
||||
packet = self.clientbound_packets[packet_id]()
|
||||
packet.read(stream)
|
||||
return packet
|
||||
else:
|
||||
print "Unkown packet: " + str(packet_id) + " / " + hex(packet_id)
|
||||
# TODO: remove debug
|
||||
print "Unknown packet: " + str(packet_id) + " / " + hex(packet_id) + " (Size: " + str(length) + ")"
|
||||
|
||||
# if this is a compressed packet then we've already read it from the stream
|
||||
# otherwise we need to skip the rest of the bytes properly
|
||||
if self.connection.options.compression_enabled and compressed_size > 0:
|
||||
return Packet()
|
||||
|
||||
# if compression is enabled and the data isn't compressed then we need to
|
||||
# subtract the size of the compressed_size and packet_id VarInts from the total data length
|
||||
# to get the number of bytes to skip
|
||||
if self.connection.options.compression_enabled and compressed_size == 0:
|
||||
real_stream.read(length - (VarInt.size(compressed_size) + VarInt.size(packet_id)))
|
||||
return Packet()
|
||||
|
||||
# If compression isn't enabled, just subtract the size of the packet_id VarInt we read
|
||||
# from the total length of the packet
|
||||
real_stream.read(length - VarInt.size(packet_id))
|
||||
|
||||
return Packet()
|
||||
else:
|
||||
return None
|
||||
|
||||
def react(self, packet):
|
||||
pass
|
||||
raise NotImplementedError("Call to base reactor")
|
||||
|
||||
|
||||
class HandshakeReactor(PacketReactor):
|
||||
clientbound_packets = state_handshake_clientbound
|
||||
class LoginReactor(PacketReactor):
|
||||
clientbound_packets = state_login_clientbound
|
||||
|
||||
def react(self, packet):
|
||||
# TODO: Add some way to bypass encryption? (connection.options.use_encryption) Not sure if it's still possible.
|
||||
if packet.packet_name == "encryption request":
|
||||
import encryption
|
||||
|
||||
secret = encryption.generate_shared_secret()
|
||||
encrypted_token, encrypted_secret = encryption.encrypt_token_and_secret(packet.public_key,
|
||||
packet.verify_token, secret)
|
||||
|
||||
# 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)
|
||||
|
||||
authentication.make_request(url, {'accessToken': self.connection.login_response.access_token,
|
||||
'selectedProfile': self.connection.login_response.profile_id,
|
||||
'serverId': server_id})
|
||||
|
||||
encryption_response = EncryptionResponsePacket()
|
||||
encryption_response.shared_secret = encrypted_secret
|
||||
encryption_response.verify_token = encrypted_token
|
||||
|
||||
# Forced because we don't want to send this encrypted which it will be
|
||||
# if we put it in the queue as we'd have wrapped the socket and file object by then
|
||||
self.connection.write_packet(encryption_response, force=True)
|
||||
|
||||
# Enable the encryption
|
||||
cipher = encryption.create_AES_cipher(secret)
|
||||
encryptor = cipher.encryptor()
|
||||
decryptor = cipher.decryptor()
|
||||
self.connection.socket = encryption.EncryptedSocketWrapper(self.connection.socket,
|
||||
encryptor, decryptor)
|
||||
self.connection.file_object = encryption.EncryptedFileObjectWrapper(self.connection.file_object,
|
||||
decryptor)
|
||||
|
||||
if packet.packet_name == "disconnect":
|
||||
print(packet.json_data) # TODO: handle propagating this back
|
||||
|
||||
if packet.packet_name == "login success":
|
||||
self.connection.reactor = PlayingReactor(self.connection)
|
||||
|
||||
if packet.packet_name == "set compression":
|
||||
self.connection.options.compression_threshold = packet.threshold
|
||||
self.connection.options.compression_enabled = True
|
||||
|
||||
|
||||
class PlayingReactor(PacketReactor):
|
||||
clientbound_packets = state_playing_clientbound
|
||||
|
||||
def react(self, packet):
|
||||
if packet.packet_name == "set compression":
|
||||
self.connection.options.compression_threshold = packet.threshold
|
||||
self.connection.options.compression_enabled = True
|
||||
|
||||
|
||||
class StatusReactor(PacketReactor):
|
||||
|
81
network/encryption.py
Normal file
81
network/encryption.py
Normal file
@ -0,0 +1,81 @@
|
||||
import os
|
||||
from hashlib import sha1
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
|
||||
from cryptography.hazmat.primitives.serialization import load_der_public_key
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
|
||||
|
||||
def generate_shared_secret():
|
||||
return os.urandom(16)
|
||||
|
||||
|
||||
def create_AES_cipher(shared_secret):
|
||||
cipher = Cipher(algorithms.AES(shared_secret), modes.CFB8(shared_secret), backend=default_backend())
|
||||
return cipher
|
||||
|
||||
|
||||
def encrypt_token_and_secret(pubkey, verification_token, shared_secret):
|
||||
"""Encrypts the verification token and shared secret with the server's public key
|
||||
|
||||
:param pubkey: The RSA public key provided by the server
|
||||
:param verification_token: The verification token provided by the server
|
||||
:param shared_secret: The generated shared secret
|
||||
:return: A tuple containing (encrypted token, encrypted secret)
|
||||
"""
|
||||
pubkey = load_der_public_key(pubkey, default_backend())
|
||||
|
||||
if not isinstance(pubkey, rsa.RSAPublicKey):
|
||||
raise RuntimeError("Public key provided by server not an RSA key")
|
||||
|
||||
encrypted_token = pubkey.encrypt(verification_token, PKCS1v15())
|
||||
encrypted_secret = pubkey.encrypt(shared_secret, PKCS1v15())
|
||||
return encrypted_token, encrypted_secret
|
||||
|
||||
|
||||
def generate_verification_hash(server_id, shared_secret, public_key):
|
||||
verification_hash = sha1()
|
||||
|
||||
verification_hash.update(server_id)
|
||||
verification_hash.update(shared_secret)
|
||||
verification_hash.update(public_key)
|
||||
|
||||
# Minecraft first parses the sha1 bytes as a signed number and then spits outs
|
||||
# its hex representation
|
||||
number = _number_from_bytes(verification_hash.digest(), signed=True)
|
||||
return format(number, 'x')
|
||||
|
||||
|
||||
def _number_from_bytes(b, signed=False):
|
||||
if len(b) == 0:
|
||||
b = b'\x00'
|
||||
num = int(str(b).encode('hex'), 16)
|
||||
if signed and (ord(b[0]) & 0x80):
|
||||
num -= 2 ** (len(b) * 8)
|
||||
return num
|
||||
|
||||
|
||||
class EncryptedFileObjectWrapper(object):
|
||||
def __init__(self, file_object, decryptor):
|
||||
self.actual_file_object = file_object
|
||||
self.decryptor = decryptor
|
||||
|
||||
def read(self, length):
|
||||
return self.decryptor.update(self.actual_file_object.read(length))
|
||||
|
||||
|
||||
class EncryptedSocketWrapper(object):
|
||||
def __init__(self, socket, encryptor, decryptor):
|
||||
self.actual_socket = socket
|
||||
self.encryptor = encryptor
|
||||
self.decryptor = decryptor
|
||||
|
||||
def recv(self, length):
|
||||
return self.decryptor.update(self.actual_socket.recv(length))
|
||||
|
||||
def send(self, data):
|
||||
self.actual_socket.send(self.encryptor.update(data))
|
||||
|
||||
def fileno(self):
|
||||
return self.actual_socket.fileno()
|
@ -1,4 +1,5 @@
|
||||
from io import BytesIO
|
||||
from zlib import compress
|
||||
|
||||
from types import *
|
||||
|
||||
@ -9,15 +10,26 @@ class PacketBuffer(object):
|
||||
self.b = BytesIO()
|
||||
|
||||
def send(self, value):
|
||||
"""
|
||||
Writes the given bytes to the buffer, designed to emulate socket.send
|
||||
:param value: The bytes to write
|
||||
"""
|
||||
self.b.write(value)
|
||||
|
||||
def read(self, length):
|
||||
return self.b.read(length)
|
||||
|
||||
def reset_cursor(self):
|
||||
self.b.seek(0)
|
||||
|
||||
def get_writable(self):
|
||||
return self.b.getvalue()
|
||||
|
||||
|
||||
class Packet(object):
|
||||
|
||||
name = "base"
|
||||
packet_name = "base"
|
||||
id = -0x01
|
||||
definition = []
|
||||
|
||||
def __init__(self):
|
||||
@ -26,12 +38,13 @@ class Packet(object):
|
||||
def read(self, file_object):
|
||||
for field in self.definition:
|
||||
for var_name, data_type in field.iteritems():
|
||||
setattr(self, var_name, data_type.read(file_object))
|
||||
value = data_type.read(file_object)
|
||||
setattr(self, var_name, value)
|
||||
|
||||
def write(self, socket):
|
||||
def write(self, socket, compression_threshold=-1):
|
||||
# buffer the data since we need to know the length of each packet's payload
|
||||
packet_buffer = PacketBuffer()
|
||||
# write off the id right off the bat
|
||||
# write packet's id right off the bat in the header
|
||||
VarInt.send(self.id, packet_buffer)
|
||||
|
||||
for field in self.definition:
|
||||
@ -39,6 +52,8 @@ class Packet(object):
|
||||
data = getattr(self, var_name)
|
||||
data_type.send(data, packet_buffer)
|
||||
|
||||
# TODO: implement compression
|
||||
|
||||
VarInt.send(len(packet_buffer.get_writable()), socket) # Packet Size
|
||||
socket.send(packet_buffer.get_writable()) # Packet Payload
|
||||
|
||||
@ -47,7 +62,7 @@ class Packet(object):
|
||||
# ==============
|
||||
class HandShakePacket(Packet):
|
||||
id = 0x00
|
||||
name = "handshake"
|
||||
packet_name = "handshake"
|
||||
definition = [
|
||||
{'protocol_version': VarInt},
|
||||
{'server_address': String},
|
||||
@ -64,21 +79,20 @@ state_handshake_serverbound = {
|
||||
|
||||
|
||||
# Status State
|
||||
#==============
|
||||
# ==============
|
||||
class ResponsePacket(Packet):
|
||||
id = 0x00
|
||||
name = "response"
|
||||
packet_name = "response"
|
||||
definition = [
|
||||
{'json_response': String}]
|
||||
|
||||
|
||||
class PingPacket(Packet):
|
||||
id = 0x01
|
||||
name = "ping"
|
||||
packet_name = "ping"
|
||||
definition = [
|
||||
{'time': Long}]
|
||||
|
||||
|
||||
state_status_clientbound = {
|
||||
0x00: ResponsePacket,
|
||||
0x01: PingPacket
|
||||
@ -87,71 +101,118 @@ state_status_clientbound = {
|
||||
|
||||
class RequestPacket(Packet):
|
||||
id = 0x00
|
||||
name = "request"
|
||||
packet_name = "request"
|
||||
definition = []
|
||||
|
||||
|
||||
class PingPacket(Packet):
|
||||
id = 0x01
|
||||
name = "ping"
|
||||
packet_name = "ping"
|
||||
definition = [
|
||||
{'time': Long}]
|
||||
|
||||
|
||||
state_status_serverbound = {
|
||||
0x00: RequestPacket,
|
||||
0x01: PingPacket
|
||||
}
|
||||
|
||||
|
||||
# Login State
|
||||
#==============
|
||||
# ==============
|
||||
class DisconnectPacket(Packet):
|
||||
id = 0x00
|
||||
name = "disconnect"
|
||||
packet_name = "disconnect"
|
||||
definition = [
|
||||
{'json_data': String}]
|
||||
|
||||
|
||||
class EncryptionRequestPacket(Packet):
|
||||
id = 0x01
|
||||
name = "encryption request"
|
||||
packet_name = "encryption request"
|
||||
definition = [
|
||||
{'server_id': String},
|
||||
{'public_key': ByteArray},
|
||||
{'verify_token': ByteArray}]
|
||||
{'public_key': VarIntPrefixedByteArray},
|
||||
{'verify_token': VarIntPrefixedByteArray}]
|
||||
|
||||
|
||||
class LoginSucessPacket(Packet):
|
||||
class LoginSuccessPacket(Packet):
|
||||
id = 0x02
|
||||
name = "login success"
|
||||
packet_name = "login success"
|
||||
definition = [
|
||||
{'UUID': String},
|
||||
{'Username': String}]
|
||||
|
||||
|
||||
class SetCompressionPacket(Packet):
|
||||
id = 0x03
|
||||
packet_name = "set compression"
|
||||
definition = [
|
||||
{'threshold': VarInt}]
|
||||
|
||||
state_login_clientbound = {
|
||||
0x00: DisconnectPacket,
|
||||
0x01: EncryptionRequestPacket,
|
||||
0x02: LoginSucessPacket
|
||||
0x02: LoginSuccessPacket,
|
||||
0x03: SetCompressionPacket
|
||||
}
|
||||
|
||||
|
||||
class LoginStartPacket(Packet):
|
||||
id = 0x00
|
||||
name = "login start"
|
||||
packet_name = "login start"
|
||||
definition = [
|
||||
{'name': String}]
|
||||
|
||||
|
||||
class EncryptionResponsePacket(Packet):
|
||||
id = 0x01
|
||||
name = "encryption response"
|
||||
packet_name = "encryption response"
|
||||
definition = [
|
||||
{'shared_secret': ByteArray},
|
||||
{'verify_token': ByteArray}]
|
||||
|
||||
{'shared_secret': VarIntPrefixedByteArray},
|
||||
{'verify_token': VarIntPrefixedByteArray}]
|
||||
|
||||
state_login_serverbound = {
|
||||
0x00: LoginStartPacket,
|
||||
0x01: EncryptionResponsePacket
|
||||
}
|
||||
|
||||
|
||||
# Playing State
|
||||
# ==============
|
||||
|
||||
class KeepAlivePacket(Packet):
|
||||
id = 0x00
|
||||
packet_name = "keep alive"
|
||||
definition = [
|
||||
{'keep_alive_id': VarInt}]
|
||||
|
||||
|
||||
class JoinGamePacket(Packet):
|
||||
id = 0x01
|
||||
packet_name = "join game"
|
||||
definition = [
|
||||
{'entity_id': Integer},
|
||||
{'game_mode': UnsignedByte},
|
||||
{'dimension': Byte},
|
||||
{'difficulty': UnsignedByte},
|
||||
{'max_players': UnsignedByte},
|
||||
{'level_type': String},
|
||||
{'reduced_debug_info': Boolean}]
|
||||
|
||||
|
||||
class SetCompressionPacketPlayState(Packet):
|
||||
id = 0x46
|
||||
packet_name = "set compression"
|
||||
definition = [
|
||||
{'threshold': VarInt}]
|
||||
|
||||
|
||||
state_playing_clientbound = {
|
||||
0x00: KeepAlivePacket,
|
||||
0x01: JoinGamePacket,
|
||||
0x46: SetCompressionPacketPlayState
|
||||
}
|
||||
|
||||
state_playing_serverbound = {
|
||||
|
||||
}
|
@ -28,6 +28,16 @@ class Boolean(Type):
|
||||
socket.send(struct.pack('?', value))
|
||||
|
||||
|
||||
class UnsignedByte(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
return struct.unpack('>B', file_object.read(1))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
socket.send(struct.pack('>B', value))
|
||||
|
||||
|
||||
class Byte(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
@ -103,6 +113,28 @@ class VarInt(Type):
|
||||
break
|
||||
socket.send(o)
|
||||
|
||||
@staticmethod
|
||||
def size(value):
|
||||
for max_value, size in VarInt_size_table.iteritems():
|
||||
if value < max_value:
|
||||
return size
|
||||
|
||||
# Maps (maximum integer value -> size of VarInt in bytes)
|
||||
VarInt_size_table = {
|
||||
2**7: 1,
|
||||
2**14: 2,
|
||||
2**21: 3,
|
||||
2**28: 4,
|
||||
2**35: 5,
|
||||
2**42: 6,
|
||||
2**49: 7,
|
||||
2**56: 8,
|
||||
2**63: 9,
|
||||
2**70: 10,
|
||||
2**77: 11,
|
||||
2**84: 12
|
||||
}
|
||||
|
||||
|
||||
class Long(Type):
|
||||
@staticmethod
|
||||
@ -134,7 +166,7 @@ class Double(Type):
|
||||
socket.send(struct.pack('>d', value))
|
||||
|
||||
|
||||
class ByteArray(Type):
|
||||
class ShortPrefixedByteArray(Type):
|
||||
@staticmethod
|
||||
def read(file_object, length=None):
|
||||
if length is None:
|
||||
@ -147,6 +179,19 @@ class ByteArray(Type):
|
||||
socket.send(value)
|
||||
|
||||
|
||||
class VarIntPrefixedByteArray(Type):
|
||||
@staticmethod
|
||||
def read(file_object, length=None):
|
||||
if length is None:
|
||||
length = VarInt.read(file_object)
|
||||
return struct.unpack(str(length) + "s", file_object.read(length))[0]
|
||||
|
||||
@staticmethod
|
||||
def send(value, socket):
|
||||
VarInt.send(len(value), socket)
|
||||
socket.send(struct.pack(str(len(value)) + "s", value))
|
||||
|
||||
|
||||
class String(Type):
|
||||
@staticmethod
|
||||
def read(file_object):
|
||||
|
@ -1 +1 @@
|
||||
pycrypto>=2.5
|
||||
cryptography
|
||||
|
17
start.py
17
start.py
@ -5,9 +5,6 @@ from optparse import OptionParser
|
||||
import authentication
|
||||
|
||||
|
||||
PROTOCOL_VERSION = 5
|
||||
|
||||
|
||||
def main():
|
||||
parser = OptionParser()
|
||||
|
||||
@ -28,13 +25,13 @@ def main():
|
||||
if not options.password:
|
||||
options.password = getpass.getpass("Enter your password: ")
|
||||
|
||||
login_response = authentication.login_to_minecraft(options.username, options.password)
|
||||
from pprint import pprint # TODO: remove debug
|
||||
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
|
||||
|
||||
if login_response.error:
|
||||
print login_response.human_error
|
||||
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)
|
||||
@ -53,7 +50,7 @@ def main():
|
||||
from network.connection import Connection
|
||||
|
||||
connection = Connection(address, port, login_response)
|
||||
connection.status()
|
||||
connection.connect()
|
||||
|
||||
while True:
|
||||
try:
|
||||
|
Loading…
Reference in New Issue
Block a user