diff --git a/lib/apps/tcp_listener.ex b/lib/apps/tcp_listener.ex index d7594f9..ccc9d2f 100644 --- a/lib/apps/tcp_listener.ex +++ b/lib/apps/tcp_listener.ex @@ -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) diff --git a/lib/data.ex b/lib/data.ex index 3235f73..37afc44 100644 --- a/lib/data.ex +++ b/lib/data.ex @@ -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 <> = <> # 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, <>, :reversed}) do + {[UUID.binary_to_string!(uuid) | acc], rest, :reversed} + end + + def raw({acc, data, :reversed}, amount) do + <> = 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))) diff --git a/lib/servers/stage1.ex b/lib/servers/handhsake.ex similarity index 95% rename from lib/servers/stage1.ex rename to lib/servers/handhsake.ex index 9e60f0f..7722eaf 100644 --- a/lib/servers/stage1.ex +++ b/lib/servers/handhsake.ex @@ -14,9 +14,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -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 diff --git a/lib/servers/login.ex b/lib/servers/login.ex new file mode 100644 index 0000000..056d231 --- /dev/null +++ b/lib/servers/login.ex @@ -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 . + +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 <> <> + 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) <> <> + 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 diff --git a/lib/servers/status.ex b/lib/servers/status.ex index 5fc2d29..b916948 100644 --- a/lib/servers/status.ex +++ b/lib/servers/status.ex @@ -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 diff --git a/mix.exs b/mix.exs index 8c18e4b..bed6248 100644 --- a/mix.exs +++ b/mix.exs @@ -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 diff --git a/mix.lock b/mix.lock index fe7f552..15dbe6b 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, }