# Amethyst - An experimental Minecraft server written in Elixir. # Copyright (C) 2024 KodiCraft # # 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 . defmodule Amethyst.Server.Login do @moduledoc """ This module contains the logic for the Handshake stage of the server. """ require Logger use Amethyst.Server alias Amethyst.Minecraft.Read alias Amethyst.Minecraft.Write ## DESERIALIZATION # Login Start https://wiki.vg/Protocol#Login_Start def deserialize(0x00, data) do {[name, uuid], ""} = Read.start(data) |> Read.string() |> Read.uuid() |> Read.stop() {:login_start, name, uuid} end # Encryption Response https://wiki.vg/Protocol#Encryption_Response def deserialize(0x01, data) do {[secret_length], rest} = Read.start(data) |> Read.varint() |> Read.stop() {[secret, verify_token_length], rest} = Read.start(rest) |> Read.raw(secret_length) |> Read.varint() |> Read.stop() {[verify_token], ""} = Read.start(rest) |> Read.raw(verify_token_length) |> Read.stop() {:encryption_response, secret, verify_token} end # Login Plugin Response https://wiki.vg/Protocol#Login_Plugin_Response def deserialize(0x02, data) do {[message_id, success], rest} = Read.start(data) |> Read.varint() |> Read.bool() |> Read.stop() if success do {:login_plugin_response, message_id, rest} else {:login_plugin_response, message_id, nil} end end # Login Acknowledged https://wiki.vg/Protocol#Login_Acknowledged def deserialize(0x03, "") do {:login_acknowledged} end # Cookie Response https://wiki.vg/Protocol#Cookie_Response_(login) def deserialize(0x04, data) do {[key, exists], rest} = Read.start(data) |> Read.string() |> Read.bool() |> Read.stop() if exists do {[length], rest} = Read.start(rest) |> Read.varint() |> Read.stop() {[data], _} = Read.start(rest) |> Read.raw(length) |> Read.stop() {:cookie_response, key, data} else {:cookie_response, key, nil} end end def deserialize(type, _) do raise RuntimeError, "Got unknown packet type #{type}!" end ## SERIALIZATION # Disconnect (login) https://wiki.vg/Protocol#Disconnect_(login) def serialize({:disconnect, reason}) do Write.varint(0x00) <> Write.string(reason) end # Encryption Request https://wiki.vg/Protocol#Encryption_Request def serialize({:encryption_request, server_id, pubkey, verify_token, auth}) do Write.varint(0x01) <> Write.string(server_id) <> Write.varint(byte_size(pubkey)) <> pubkey <> Write.varint(byte_size(verify_token)) <> verify_token <> Write.bool(auth) end # Login Success https://wiki.vg/Protocol#Login_Success def serialize({:login_success, uuid, username, props, strict}) do Write.varint(0x02) <> Write.uuid(uuid) <> Write.string(username) <> Write.varint(length(props)) <> Enum.reduce(props, "", fn {name, value, signature}, acc -> acc <> Write.string(name) <> Write.string(value) <> case signature do nil -> <<0x00>> signature -> <<0x01>> <> Write.string(signature) end end) <> Write.bool(strict) end # Set Compression https://wiki.vg/Protocol#Set_Compression def serialize({:set_compression, threshold}) do Write.varint(0x03) <> Write.varint(threshold) end # Login Plugin Request https://wiki.vg/Protocol#Login_Plugin_Request def serialize({:login_plugin_request, id, channel, data}) do Write.varint(0x04) <> Write.varint(id) <> Write.string(channel) <> data end # Cookie Request (login) https://wiki.vg/Protocol#Cookie_Request_(login) def serialize({:cookie_request_login, id}) do Write.varint(0x05) <> Write.string(id) end def serialize(packet) do raise ArgumentError, "Tried serializing unknown packet #{inspect(packet)}" end ## HANDLING # Login Start https://wiki.vg/Protocol#Login_Start def handle({:login_start, name, uuid}, client) do Logger.info("Logging in #{name} (#{uuid})") if Application.fetch_env!(:amethyst, :encryption) do raise RuntimeError, "Encryption is currently unsupported." # TODO: Implement encryption # verify_token = :crypto.strong_rand_bytes(4) # pubkey = Amethyst.Keys.get_pub() # auth = Application.fetch_env!(:amethyst, :auth) # transmit({:encryption_request, "amethyst", pubkey, verify_token, auth}, client) # This is broken for some reason? java.lang.IllegalStateException: Protocol Error else transmit({:login_success, uuid, name, [], false}, client) end end def handle({:login_acknowledged}, _client) do raise RuntimeError, "Configuration stage is not implemented." end def handle(tuple, _) do Logger.error("Unhandled but known packet #{elem(tuple, 0)}") end end