Implementing login messages

The login message data can be found in the intermediate_representation.json file. The intermediate_representation_schema.json contains a JSON type def schema for the intermediate_representation.json. The json-typedef-codegen program can be used to generate bindings for many popular languages.

The wow_messages_python repository contains the Python code used to generate the Python message library. It can be used as inspiration for your own implementation. The generator directory contains the actual code generation, and the wow_login_messages directory contains the generated library.

Python code will be shown in this document to provide examples of how libraries could be implemented. If you want to use the Python library then just bypass this and use the library directly instead. A C# library is available here.

Message Layout

All interactions start with the client sending a message, then reading the reply from the server.

All login messages start with an opcode field field that specifies the message contents. This is the only thing the messages have in common.

The only way to know how much data to expect in a message is by a combination of the protocol version sent in the very first message by the client, CMD_AUTH_LOGON_CHALLENGE_Client, and the opcode.

Take a look at the wiki page on login in order to get an understanding for what is going to happen.

Types used for login messages

Login messages use the following types, including enums, flags, and structs:

TypePurposeC Name
u8Unsigned 8 bit integer. Min value 0, max value 256.unsigned char
u16Unsigned 16 bit integer. Min value 0, max value 65536.unsigned short
u32Unsigned 32 bit integer. Min value 0, max value 4294967296.unsigned int
u64Unsigned 64 bit integer. Min value 0, max value 18446744073709551616.unsigned long long
BoolUnsigned 1 bit integer. 0 means false and all other values mean true.unsigned char
CStringUTF-8 string type that is terminated by a zero byte value.char*
StringUTF-8 string type of exactly length len.unsigned char + char*
Populationf32 with the special behavior that a value of 200 always means GREEN_RECOMMENDED, 400 always means RED_FULL, and 600 always means BLUE_RECOMMENDED.float
IpAddressAlias for big endian u32.unsigned int

Library layout

Consider what you want the layout of the library to look like regarding files and directories.

There are several different protocol versions, objects that are valid for more than one version, as well as objects that are valid for all versions. Consider if your programming language benefits from having a type that is identical, but for a different version actually be the same type.

For the Python library, I chose to have a Python file for each version, as well as one for all. Objects that are valid for more than one version are reexported from the lowest version.

The folder layout for the Python library is as follows:

├── README.md
└── wow_login_messages
    ├── all.py
    ├── __init__.py
    ├── opcodes.py
    ├── util.py
    ├── version2.py
    ├── version3.py
    ├── version5.py
    ├── version6.py
    ├── version7.py
    └── version8.py

The __init__.py file is what is imported when just importing wow_login_messages. The opcodes.py file just contains the opcode values for the different messages. The util.py file contains utility functions intended to be used by the user of the library, such as a function that reads the opcode and automatically reads the remaining fields and returns the message as a Python class.

Enums

Enums can only have a single enumerator value, and it can not have any other values than those specifically described. Consider how you would implement and use the ProtocolVersion enum in your programming language.

Newer Python versions have the enum module, which provide an easy way of creating enum types.

So the ProtocolVersion enum in Python looks like:

import enum


class ProtocolVersion(enum.Enum):
    TWO = 2
    THREE = 3
    FIVE = 5
    SIX = 6
    SEVEN = 7
    EIGHT = 8


protocol_version = ProtocolVersion(2)
value = protocol_version.value

This simple snippet allows construction of ProtocolVersion types through ProtocolVersion(2) and getting the value of an enumerator through protocol_version.value.

I have chosen not to include a read or write function on the enum, since this is handled by the object that needs to read/write the enum.

The python library places all imports at the top of the file, and all objects for the same version are in the same file, so it is not necessary to import enum for every enum.

Flags

Flags are like enums, but can have multiple enumerator values at the same time, or none of them. Consider how you would implement and use the RealmFlag flag in your programming language.

The enum module used for enums can also be used for flags.

So the RealmFlag enum in Python looks like:

import enum


class RealmFlag(enum.Flag):
    NONE = 0
    INVALID = 1
    OFFLINE = 2
    FORCE_BLUE_RECOMMENDED = 32
    FORCE_GREEN_RECOMMENDED = 64
    FORCE_RED_FULL = 128


realm_flag = RealmFlag(32) | RealmFlag(2)

assert realm_flag.OFFLINE
assert realm_flag.FORCE_BLUE_RECOMMENDED

value = realm_flag.value

This snippet allows the construction of RealmFlag through RealmFlag(32), the bitwise or through the | operator and getting the value through realm_flag.value.

Flags also do not handle reading and writing, that is handled by the objects containing them, although it could be different in your programming language.

CMD_AUTH_LOGON_CHALLENGE_Client

This is the first message sent by the client. It is sent automatically upon connecting.

It is the only message to have the String type, which is a single length byte (u8) followed by as many bytes as the length byte says of string. All other messages use the CString type, which is an arbitrary amount of bytes terminated by a 0 byte.

It is the only message that contains the IpAddress type alias. This is not a "real" type, but simply a u32 that should be interpreted as an IP address if the programming language has built in ways of dealing with IP addresses.

It contains the ProtocolVersion enum, which determines which version of messages is used.

It contains the Version struct.

For writing a server, it requires being able to be read, and for clients it requires being able to be written. When sent by clients, it has to calculate how large the actual message being sent is, and put it in the size field.

In Python, the definition would be:

import dataclasses


@dataclasses.dataclass
class CMD_AUTH_LOGON_CHALLENGE_Client:
    protocol_version: ProtocolVersion
    version: Version
    platform: Platform
    os: Os
    locale: Locale
    utc_timezone_offset: int
    client_ip_address: int
    account_name: str

The types after the colon (:) are type hints. The @dataclasses.dataclass attribute adds some niceties like automatic constructors.

All containers have read, write and _size methods. The underscore in _size is a Python way of signalling that the method should be private.

The read function looks like

@staticmethod
async def read(reader: asyncio.StreamReader):
    # protocol_version: DataTypeEnum(data_type_tag='Enum', content=DataTypeEnumContent(integer_type=<IntegerType.U8: 'U8'>, type_name='ProtocolVersion', upcast=False))
    protocol_version = ProtocolVersion(int.from_bytes(await reader.readexactly(1), 'little'))

    # size: DataTypeInteger(data_type_tag='Integer', content=<IntegerType.U16: 'U16'>)
    _size = int.from_bytes(await reader.readexactly(2), 'little')

    # game_name: DataTypeInteger(data_type_tag='Integer', content=<IntegerType.U32: 'U32'>)
    _game_name = int.from_bytes(await reader.readexactly(4), 'little')

    # version: DataTypeStruct(data_type_tag='Struct', content=DataTypeStructContent(sizes=Sizes(constant_sized=True, maximum_size=5, minimum_size=5), type_name='Version'))
    version = await Version.read(reader)

    # platform: DataTypeEnum(data_type_tag='Enum', content=DataTypeEnumContent(integer_type=<IntegerType.U32: 'U32'>, type_name='Platform', upcast=False))
    platform = Platform(int.from_bytes(await reader.readexactly(4), 'little'))

    # os: DataTypeEnum(data_type_tag='Enum', content=DataTypeEnumContent(integer_type=<IntegerType.U32: 'U32'>, type_name='Os', upcast=False))
    os = Os(int.from_bytes(await reader.readexactly(4), 'little'))

    # locale: DataTypeEnum(data_type_tag='Enum', content=DataTypeEnumContent(integer_type=<IntegerType.U32: 'U32'>, type_name='Locale', upcast=False))
    locale = Locale(int.from_bytes(await reader.readexactly(4), 'little'))

    # utc_timezone_offset: DataTypeInteger(data_type_tag='Integer', content=<IntegerType.U32: 'U32'>)
    utc_timezone_offset = int.from_bytes(await reader.readexactly(4), 'little')

    # client_ip_address: DataTypeIPAddress(data_type_tag='IpAddress')
    client_ip_address = int.from_bytes(await reader.readexactly(4), 'big')

    # account_name: DataTypeString(data_type_tag='String')
    account_name = int.from_bytes(await reader.readexactly(1), 'little')
    account_name = (await reader.readexactly(account_name)).decode('utf-8')

    return CMD_AUTH_LOGON_CHALLENGE_Client(
        protocol_version,
        version,
        platform,
        os,
        locale,
        utc_timezone_offset,
        client_ip_address,
        account_name,
    )

It reads every member in sequence before returning an object with all the fields. The comments print the name and type of variables, in order to make debugging easier.

This showcases how enums are ready by reading an appropriately sized integer and then passing it into the enum constructor.

The struct Version is read by calling a read function defined on that object.

The IpAddress is simply read as an integer.

Python allows changing the type of a variable, so the account_name variable is first used for the length, and then for the contents.

_size is not included in the object, since manually modifying the size is error prone and tedious. It is instead calculated automatically in write. _game_name is not included in the object since it has a constant value.

The write function looks like

def write(self, writer: asyncio.StreamWriter):
    fmt = '<B'  # opcode
    data = [0]

    # protocol_version: DataTypeEnum(data_type_tag='Enum', content=DataTypeEnumContent(integer_type=<IntegerType.U8: 'U8'>, type_name='ProtocolVersion', upcast=False))
    fmt += 'B'
    data.append(self.protocol_version.value)

    # size: DataTypeInteger(data_type_tag='Integer', content=<IntegerType.U16: 'U16'>)
    fmt += 'H'
    data.append(self.size())

    # game_name: DataTypeInteger(data_type_tag='Integer', content=<IntegerType.U32: 'U32'>)
    fmt += 'I'
    data.append(5730135)

    # version: DataTypeStruct(data_type_tag='Struct', content=DataTypeStructContent(sizes=Sizes(constant_sized=True, maximum_size=5, minimum_size=5), type_name='Version'))
    fmt, data = self.version.write(fmt, data)

    # platform: DataTypeEnum(data_type_tag='Enum', content=DataTypeEnumContent(integer_type=<IntegerType.U32: 'U32'>, type_name='Platform', upcast=False))
    fmt += 'I'
    data.append(self.platform.value)

    # os: DataTypeEnum(data_type_tag='Enum', content=DataTypeEnumContent(integer_type=<IntegerType.U32: 'U32'>, type_name='Os', upcast=False))
    fmt += 'I'
    data.append(self.os.value)

    # locale: DataTypeEnum(data_type_tag='Enum', content=DataTypeEnumContent(integer_type=<IntegerType.U32: 'U32'>, type_name='Locale', upcast=False))
    fmt += 'I'
    data.append(self.locale.value)

    # utc_timezone_offset: DataTypeInteger(data_type_tag='Integer', content=<IntegerType.U32: 'U32'>)
    fmt += 'I'
    data.append(self.utc_timezone_offset)

    # client_ip_address: DataTypeIPAddress(data_type_tag='IpAddress')
    fmt += 'I'
    data.append(self.client_ip_address)

    # account_name: DataTypeString(data_type_tag='String')
    fmt += f'B{len(self.account_name)}s'
    data.append(len(self.account_name))
    data.append(self.account_name.encode('utf-8'))

    data = struct.pack(fmt, *data)
    writer.write(data)

The complexity of this is because of how Pythons struct module for writing values to bytes works. The line with data = struct.pack(fmt, *data) writes the data to a byte array that is then written to the stream on the next line. Hopefully your programming language has a more sane way of writing data to a stream.

The final part is the size calculations, it looks like

def size(self):
    size = 0

    # protocol_version: DataTypeEnum(data_type_tag='Enum', content=DataTypeEnumContent(integer_type=<IntegerType.U8: 'U8'>, type_name='ProtocolVersion', upcast=False))
    size += 1

    # size: DataTypeInteger(data_type_tag='Integer', content=<IntegerType.U16: 'U16'>)
    size += 2

    # game_name: DataTypeInteger(data_type_tag='Integer', content=<IntegerType.U32: 'U32'>)
    size += 4

    # version: DataTypeStruct(data_type_tag='Struct', content=DataTypeStructContent(sizes=Sizes(constant_sized=True, maximum_size=5, minimum_size=5), type_name='Version'))
    size += 5

    # platform: DataTypeEnum(data_type_tag='Enum', content=DataTypeEnumContent(integer_type=<IntegerType.U32: 'U32'>, type_name='Platform', upcast=False))
    size += 4

    # os: DataTypeEnum(data_type_tag='Enum', content=DataTypeEnumContent(integer_type=<IntegerType.U32: 'U32'>, type_name='Os', upcast=False))
    size += 4

    # locale: DataTypeEnum(data_type_tag='Enum', content=DataTypeEnumContent(integer_type=<IntegerType.U32: 'U32'>, type_name='Locale', upcast=False))
    size += 4

    # utc_timezone_offset: DataTypeInteger(data_type_tag='Integer', content=<IntegerType.U32: 'U32'>)
    size += 4

    # client_ip_address: DataTypeIPAddress(data_type_tag='IpAddress')
    size += 4

    # account_name: DataTypeString(data_type_tag='String')
    size += len(self.account_name)

    return size - 3

This sums up the sizes of all members, and then subtracts the fields that come before the size field.

CMD_AUTH_LOGON_CHALLENGE_Server

The servers reply to the client has the same opcode as the initial message, and it provides the first example of a message that has control flow (if statements).

@dataclasses.dataclass
class CMD_AUTH_LOGON_CHALLENGE_Server:
    result: LoginResult
    server_public_key: typing.Optional[typing.List[int]]
    generator: typing.Optional[typing.List[int]]
    large_safe_prime: typing.Optional[typing.List[int]]
    salt: typing.Optional[typing.List[int]]
    crc_salt: typing.Optional[typing.List[int]]

The Python version solves this issue by making every variable that is not certain to be in the message Optional. This type hint means that the variables can also be None, and have no value of their actual type.

The python code for reading reads the result and then branches based on the value.

    @staticmethod


async def read(reader: asyncio.StreamReader):
    server_public_key = None
    generator_length = None
    generator = None
    large_safe_prime_length = None
    large_safe_prime = None
    salt = None
    crc_salt = None

    # protocol_version: DataTypeInteger(data_type_tag='Integer', content=<IntegerType.U8: 'U8'>)
    _protocol_version = int.from_bytes(await reader.readexactly(1), "little")

    # result: DataTypeEnum(data_type_tag='Enum', content=DataTypeEnumContent(integer_type=<IntegerType.U8: 'U8'>, type_name='LoginResult', upcast=False))
    result = LoginResult(int.from_bytes(await reader.readexactly(1), "little"))

    if result == LoginResult.SUCCESS:
        # server_public_key: DataTypeArray(data_type_tag='Array', content=Array(inner_type=ArrayTypeInteger(array_type_tag='Integer', inner_type=<IntegerType.U8: 'U8'>), size=ArraySizeFixed(array_size_tag='Fixed', size='32')))
        server_public_key = []
        for _ in range(0, 32):
            server_public_key.append(
                int.from_bytes(await reader.readexactly(1), "little")
            )

        # generator_length: DataTypeInteger(data_type_tag='Integer', content=<IntegerType.U8: 'U8'>)
        generator_length = int.from_bytes(await reader.readexactly(1), "little")

        # generator: DataTypeArray(data_type_tag='Array', content=Array(inner_type=ArrayTypeInteger(array_type_tag='Integer', inner_type=<IntegerType.U8: 'U8'>), size=ArraySizeVariable(array_size_tag='Variable', size='generator_length')))
        generator = []
        for _ in range(0, generator_length):
            generator.append(int.from_bytes(await reader.readexactly(1), "little"))

        # large_safe_prime_length: DataTypeInteger(data_type_tag='Integer', content=<IntegerType.U8: 'U8'>)
        large_safe_prime_length = int.from_bytes(
            await reader.readexactly(1), "little"
        )

        # large_safe_prime: DataTypeArray(data_type_tag='Array', content=Array(inner_type=ArrayTypeInteger(array_type_tag='Integer', inner_type=<IntegerType.U8: 'U8'>), size=ArraySizeVariable(array_size_tag='Variable', size='large_safe_prime_length')))
        large_safe_prime = []
        for _ in range(0, large_safe_prime_length):
            large_safe_prime.append(
                int.from_bytes(await reader.readexactly(1), "little")
            )

        # salt: DataTypeArray(data_type_tag='Array', content=Array(inner_type=ArrayTypeInteger(array_type_tag='Integer', inner_type=<IntegerType.U8: 'U8'>), size=ArraySizeFixed(array_size_tag='Fixed', size='32')))
        salt = []
        for _ in range(0, 32):
            salt.append(int.from_bytes(await reader.readexactly(1), "little"))

        # crc_salt: DataTypeArray(data_type_tag='Array', content=Array(inner_type=ArrayTypeInteger(array_type_tag='Integer', inner_type=<IntegerType.U8: 'U8'>), size=ArraySizeFixed(array_size_tag='Fixed', size='16')))
        crc_salt = []
        for _ in range(0, 16):
            crc_salt.append(int.from_bytes(await reader.readexactly(1), "little"))

    return CMD_AUTH_LOGON_CHALLENGE_Server(
        result,
        server_public_key,
        generator,
        large_safe_prime,
        salt,
        crc_salt,
    )

CMD_AUTH_LOGON_PROOF_Client

This message is much like the others, but it contains an array of structs.

CMD_REALM_LIST_Client

This message is not empty, but has a padding variable that should always be a constant value, so it becomes empty if constant values are removed.

CMD_XFER_ACCEPT

This message has an empty body.

CMD_REALM_LIST_Server

This message has a bunch of padding and an array of Realm structs, as well as having a size field.

Realm

This struct has several CString variables, which are read by reading until finding a 0 byte.

It also has the Population alias, which can just be substituted for a 4 byte floating point value.

Version 8 additionally has an if statement that uses a flag instead of an enum.