Begin implementing Login stage

This commit is contained in:
Kodi Craft 2024-07-08 16:57:49 +02:00
parent 1ce9b7c454
commit 809a37c644
Signed by: kodi
GPG Key ID: 69D9EED60B242822
7 changed files with 126 additions and 6 deletions

View File

@ -29,7 +29,7 @@ defmodule Amethyst.TCPListener do
@spec loop_acceptor(socket :: :gen_tcp.socket()) :: no_return()
defp loop_acceptor(socket) do
{:ok, client} = :gen_tcp.accept(socket)
{:ok, pid} = Task.Supervisor.start_child(Amethyst.ConnectionSupervisor, fn -> Amethyst.Server.Stage1.serve(client) end)
{:ok, pid} = Task.Supervisor.start_child(Amethyst.ConnectionSupervisor, fn -> Amethyst.Server.Handshake.serve(client) end)
:ok = :gen_tcp.controlling_process(client, pid)
Logger.info("Received connection from #{inspect(client)}, assigned to PID #{inspect(pid)}")
loop_acceptor(socket)

View File

@ -22,6 +22,10 @@ defmodule Amethyst.Minecraft.Write do
than simple binary data.
"""
def uuid(uuid) do
UUID.string_to_binary!(uuid)
end
def varint(value) when value in -2_147_483_648..2_147_483_647 do
<<value::32-unsigned>> = <<value::32-signed>> # This is a trick to allow the arithmetic shift to act as a logical shift
varnum("", value)
@ -115,6 +119,15 @@ defmodule Amethyst.Minecraft.Read do
{[value | acc], rest, :reversed}
end
def uuid({acc, <<uuid::16-binary, rest::binary>>, :reversed}) do
{[UUID.binary_to_string!(uuid) | acc], rest, :reversed}
end
def raw({acc, data, :reversed}, amount) do
<<data::binary-size(amount), rest::binary>> = data
{[data | acc], rest, :reversed}
end
def varint(tuple, read \\ 0, nacc \\ 0)
def varint({acc, <<1::1, value::7, rest::binary>>, :reversed}, read, nacc) when read < 5 do
varint({acc, rest, :reversed}, read + 1, nacc + (value <<< (7 * read)))

View File

@ -14,9 +14,9 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
defmodule Amethyst.Server.Stage1 do
defmodule Amethyst.Server.Handshake do
@moduledoc """
This module contains the stage 1 (Handshaking) server logic.
This module contains the logic for the Handshake stage of the server.
"""
require Logger
use Amethyst.Server

106
lib/servers/login.ex Normal file
View File

@ -0,0 +1,106 @@
# 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 <https://www.gnu.org/licenses/>.
defmodule Amethyst.Server.Login do
@moduledoc """
This module contains the logic for the Handshake stage of the server.
"""
require Logger
use Amethyst.Server
alias Credo.Execution.Task.WriteDebugReport
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, id, pubkey, verify_token, auth}) do
Write.varint(0x01) <> Write.string(id) <> Write.varint(byte_size(pubkey)) <> pubkey <> Write.varint(byte_size(verify_token)) <> verify_token <> <<auth::8>>
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) <> <<strict::8>>
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
def handle(tuple, _) do
Logger.error("Unhandled but known packet #{elem(tuple, 0)}")
end
end

View File

@ -16,8 +16,7 @@
defmodule Amethyst.Server.Status do
@moduledoc """
This module contains the Status logic, this is not really its own login state since the client
is only asking a bit of information.
This module contains the logic for the Status stage of the server.
"""
require Logger
use Amethyst.Server

View File

@ -22,7 +22,8 @@ defmodule Amethyst.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:uuid, "~> 1.1"}
]
end
end

View File

@ -3,4 +3,5 @@
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"},
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"},
}