Compare commits

...

28 Commits

Author SHA1 Message Date
47bb453178
"Implement" "chat"
All checks were successful
Build & Test / nix-build (push) Successful in 1m53s
2024-10-09 12:19:50 +02:00
29014aa45f
Implement receiving chat messages
All checks were successful
Build & Test / nix-build (push) Successful in 1m46s
2024-10-08 15:34:57 +02:00
7f68a95e28
Attempt to implement data for fixed bitset reading
All checks were successful
Build & Test / nix-build (push) Successful in 1m47s
2024-10-08 14:40:22 +02:00
c70098814a
Tweak credo and cleanups
All checks were successful
Build & Test / nix-build (push) Successful in 1m45s
2024-10-08 10:59:57 +02:00
9c03318e3f
More tinkering
All checks were successful
Build & Test / nix-build (push) Successful in 1m47s
2024-10-08 10:30:56 +02:00
c9a3dec823
Tinker with example game
All checks were successful
Build & Test / nix-build (push) Successful in 1m46s
2024-10-08 10:29:01 +02:00
aa91e82cfa
Remove spam
All checks were successful
Build & Test / nix-build (push) Successful in 1m45s
2024-10-08 09:10:27 +02:00
c2cbfdf888
Properly implement light... I think
All checks were successful
Build & Test / nix-build (push) Successful in 1m47s
2024-10-08 09:06:54 +02:00
b44784e9ef
Attempt implementing light
All checks were successful
Build & Test / nix-build (push) Successful in 1m49s
2024-10-07 21:05:37 +02:00
5743ae55e0
Update release system
All checks were successful
Build & Test / nix-build (push) Successful in 1m45s
2024-10-07 13:52:33 +02:00
2ef24bbe51 Merge pull request 'Implement Mojang authentication' (#4) from auth into main
All checks were successful
Build & Test / nix-build (push) Successful in 1m39s
Reviewed-on: #4
2024-10-07 11:26:31 +02:00
cec646f622
Update mix.nix
All checks were successful
Build & Test / nix-build (push) Successful in 3m39s
Build & Test / nix-build (pull_request) Successful in 2m44s
2024-10-07 11:18:22 +02:00
7805f3b225 Merge pull request 'Implement compression' (#3) from compression into main
All checks were successful
Build & Test / nix-build (push) Successful in 1m27s
Reviewed-on: #3
2024-10-06 19:51:44 +02:00
c29f95a647
Render the player's skin when possible
Some checks failed
Build & Test / nix-build (push) Failing after 18s
2024-10-06 19:03:12 +02:00
4ecf2d432f
Signatures are sent correctly 2024-10-06 18:29:05 +02:00
e45258ba6f
remove debug log 2024-10-06 18:02:54 +02:00
c598de4e7b
Initial implementation of authentication 2024-10-06 17:53:27 +02:00
8b09c78f02
Actually implement compression
All checks were successful
Build & Test / nix-build (push) Successful in 1m29s
Build & Test / nix-build (pull_request) Successful in 1m24s
2024-10-06 15:49:55 +02:00
be44916461
Implement compression 2024-10-06 15:20:39 +02:00
fb98dd4864
Attempt to implement compression 2024-10-06 13:49:14 +02:00
1e7cc32af7
Experiment with a different map
All checks were successful
Build & Test / nix-build (push) Successful in 1m29s
2024-10-05 19:03:22 +02:00
eff1ff0a5e Merge pull request 'Implement protocol encryption' (#2) from encryption into main
All checks were successful
Build & Test / nix-build (push) Successful in 3m24s
Reviewed-on: #2
2024-10-05 12:07:03 +02:00
93f8432bd1
Fix process leak
All checks were successful
Build & Test / nix-build (pull_request) Successful in 4m34s
Build & Test / nix-build (push) Successful in 4m42s
2024-10-05 12:06:33 +02:00
7f3bb357db
Implement encryption
All checks were successful
Build & Test / nix-build (push) Successful in 1m59s
Build & Test / nix-build (pull_request) Successful in 1m57s
2024-10-05 11:55:27 +02:00
3bb8c5286a
Actually implement reading byte_arrays
All checks were successful
Build & Test / nix-build (push) Successful in 1m28s
2024-10-05 11:35:38 +02:00
80060b1708
progress???
All checks were successful
Build & Test / nix-build (push) Successful in 1m28s
2024-10-05 11:27:10 +02:00
3f0e0ae22f
Some attempts at troubleshooting
All checks were successful
Build & Test / nix-build (push) Successful in 1m29s
2024-10-04 19:55:52 +02:00
c26fc23a33
Begin attempting to implement encryption
All checks were successful
Build & Test / nix-build (push) Successful in 1m29s
2024-10-04 16:02:57 +02:00
19 changed files with 614 additions and 157 deletions

View File

@ -86,6 +86,8 @@
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
{Credo.Check.Readability.VariableNames, []},
{Credo.Check.Readability.WithSingleClause, []},
{Credo.Check.Readability.BlockPipe, []},
{Credo.Check.Readability.SinglePipe, []},
#
## Refactoring Opportunities
@ -106,6 +108,8 @@
{Credo.Check.Refactor.RejectReject, []},
{Credo.Check.Refactor.UnlessWithElse, []},
{Credo.Check.Refactor.WithClauses, []},
{Credo.Check.Refactor.MapMap, []},
{Credo.Check.Refactor.FilterReject, []},
#
## Warnings
@ -146,7 +150,6 @@
{Credo.Check.Design.DuplicatedCode, []},
{Credo.Check.Design.SkipTestWithoutComment, []},
{Credo.Check.Readability.AliasAs, []},
{Credo.Check.Readability.BlockPipe, []},
{Credo.Check.Readability.ImplTrue, []},
{Credo.Check.Readability.MultiAlias, []},
{Credo.Check.Readability.NestedFunctionCalls, []},
@ -154,16 +157,13 @@
{Credo.Check.Readability.OnePipePerLine, []},
{Credo.Check.Readability.SeparateAliasRequire, []},
{Credo.Check.Readability.SingleFunctionToBlockPipe, []},
{Credo.Check.Readability.SinglePipe, []},
{Credo.Check.Readability.Specs, []},
{Credo.Check.Readability.StrictModuleLayout, []},
{Credo.Check.Readability.WithCustomTaggedTuple, []},
{Credo.Check.Refactor.ABCSize, []},
{Credo.Check.Refactor.AppendSingleItem, []},
{Credo.Check.Refactor.DoubleBooleanNegation, []},
{Credo.Check.Refactor.FilterReject, []},
{Credo.Check.Refactor.IoPuts, []},
{Credo.Check.Refactor.MapMap, []},
{Credo.Check.Refactor.ModuleDependencies, []},
{Credo.Check.Refactor.NegatedIsNil, []},
{Credo.Check.Refactor.PassAsyncInTestCases, []},
@ -174,9 +174,8 @@
{Credo.Check.Warning.LeakyEnvironment, []},
{Credo.Check.Warning.MapGetUnsafePass, []},
{Credo.Check.Warning.MixEnv, []},
{Credo.Check.Warning.UnsafeToAtom, []}
# {Credo.Check.Refactor.MapInto, []},
{Credo.Check.Warning.UnsafeToAtom, []},
{Credo.Check.Refactor.MapInto, []},
#
# Custom checks can be created using `mix credo.gen.check`.

View File

@ -19,6 +19,7 @@ defmodule Amethyst.ConnectionHandler do
This module is responsible for handling incoming packets and sending outgoing packets. It keeps track of what state the connection is in and which game should
receive the packets.
"""
alias Amethyst.Minecraft.Write
require Logger
@spec child_spec(:gen_tcp.socket()) :: Supervisor.child_spec()
@ -48,11 +49,8 @@ defmodule Amethyst.ConnectionHandler do
@spec loop(:gen_tcp.socket(), atom(), integer(), map()) :: no_return()
defp loop(socket, connstate, version, state) do
receive do
:closed ->
Logger.info("Connection #{inspect(socket)} closed.")
Process.exit(self(), :normal)
{:disconnect, reason} ->
disconnect(socket, reason, connstate, version)
disconnect(socket, reason, connstate, version, state)
Process.exit(self(), :normal)
{:set_state, newstate} ->
Logger.debug("Switching to state #{newstate} from #{connstate}")
@ -60,6 +58,16 @@ defmodule Amethyst.ConnectionHandler do
{:set_version, newversion} ->
Logger.debug("Switching to version #{newversion} from #{version}")
loop(socket, connstate, newversion, state)
{:set_encryption, secret} ->
Logger.debug("Enabling encryption with shared secret #{inspect(secret)}")
encryption_state = :crypto.crypto_init(:aes_128_cfb8, secret, secret, true)
decryption_state = :crypto.crypto_init(:aes_128_cfb8, secret, secret, false)
state = state |> Map.put(:encryption_state, encryption_state) |> Map.put(:decryption_state, decryption_state)
loop(socket, connstate, version, state)
{:set_compression, threshold} ->
Logger.debug("Enabling comrpession with threshold #{threshold}")
state = Map.put(state, :compression, threshold)
loop(socket, connstate, version, state)
{:set_position, position} ->
prev_position = Map.get(state, :position)
state = Map.put(state, :position, position)
@ -80,9 +88,9 @@ defmodule Amethyst.ConnectionHandler do
if prev_cp == nil do
MapSet.new([])
else
MapSet.new(visible_chunks_from(elem(prev_cp, 0), elem(prev_cp, 1), Map.get(state, :view_distance, 16)))
MapSet.new(visible_chunks_from(elem(prev_cp, 0), elem(prev_cp, 1), 16)) # This 16 would be the server view distance!!
end
chunks = MapSet.new(visible_chunks_from(elem(cp, 0), elem(cp, 1), Map.get(state, :view_distance, 16)))
chunks = MapSet.new(visible_chunks_from(elem(cp, 0), elem(cp, 1), 16))
new_chunks = MapSet.difference(chunks, prev_chunks)
Logger.debug("Sending #{MapSet.size(new_chunks)} chunks...")
# We can process all chunks in parallel
@ -98,10 +106,20 @@ defmodule Amethyst.ConnectionHandler do
loop(socket, connstate, version, state)
{:send_packet, packet} ->
# Logger.debug("Sending packet #{inspect(packet)}")
send_packet(socket, connstate, packet, version)
send_packet(socket, connstate, packet, version, state)
loop(socket, connstate, version, state)
after 0 ->
# Received stuff from the connection receiver is lower priority
receive do
:closed ->
Logger.info("Connection #{inspect(socket)} closed.")
Process.exit(self(), :normal)
{:get_encryption, from} ->
send(from, Map.get(state, :decryption_state))
loop(socket, connstate, version, state)
{:get_compression, from} ->
send(from, Map.get(state, :compression))
loop(socket, connstate, version, state)
{:packet, id, data} ->
state = handle_packet(id, data, connstate, version, state)
loop(socket, connstate, version, state)
@ -136,39 +154,42 @@ defmodule Amethyst.ConnectionHandler do
data = Enum.chunk_every(chunk_array, 16, 16, 0) # 0 -> air
|> Enum.reduce("", fn chunk_section, acc ->
blocks = chunk_section |> List.flatten()
block_count = blocks |> Enum.filter(&(&1 != 0)) |> length
blocks_and_lights = chunk_section |> List.flatten()
block_count = blocks_and_lights |> Enum.filter(fn {bs, _, _} -> bs != 0 end) |> length
# Put together the palette
unique_blocks = MapSet.new(blocks)
unique_blocks = MapSet.new(blocks_and_lights |> Enum.map(fn {bs, _, _} -> bs end))
min_bpe = MapSet.size(unique_blocks) |> :math.log2() |> ceil()
paletted_container_data = case min_bpe do
0 ->
# SINGLE VALUED
Write.ubyte(0) <>
Write.varint(MapSet.to_list(unique_blocks) |> List.first()) <>
Write.varint(0) # No data, empty pallette
min_bpe when min_bpe in 1..8 ->
# INDIRECT
# Minimum bpe accepted by minecraft is 4
bpe = max(min_bpe, 4)
palette = MapSet.to_list(unique_blocks) |>
Enum.with_index() |>
Map.new(fn {i, v} -> {i, v} end)
paletted_blocks = blocks |>
Enum.map(&(Map.get(palette, &1)))
paletted_data = long_aligned_bit_string_reduce(paletted_blocks, bpe)
Map.new(fn {v, i} -> {v, i} end)
paletted_blocks = blocks_and_lights |>
Enum.map(fn {bs, _, _} -> Map.get(palette, bs) end)
paletted_data = long_aligned_bit_string_reduce(paletted_blocks, bpe,
fn bs -> <<bs::signed-integer-big-size(bpe)>> end)
Write.ubyte(bpe) <>
Write.varint(map_size(palette)) <>
Enum.reduce(palette, "", fn {_k, v}, acc ->
acc <> Write.varint(v)
end) <>
(Enum.sort(palette, fn {_k1, v1}, {_k2, v2} -> v1 < v2 end) |>
Enum.reduce("", fn {k, _v}, acc -> acc <> Write.varint(k) end)) <>
Write.varint(floor(bit_size(paletted_data) / 64)) <>
paletted_data
_ ->
# DIRECT
data = long_aligned_bit_string_reduce(blocks, 15)
data = long_aligned_bit_string_reduce(blocks_and_lights, 15,
fn {bs, _sl, _bl} -> <<bs::signed-integer-big-size(15)>> end)
Write.ubyte(15) <>
Write.varint(floor(bit_size(data) / 64)) <>
data
@ -178,26 +199,48 @@ defmodule Amethyst.ConnectionHandler do
<<0::8, 0::8, 0::8>> # TODO: This should be biome data
end)
sky_light_sections = Enum.chunk_every(chunk_array, 16, 16, 0)
|> Enum.map(fn chunk_section ->
chunk_section |> List.flatten() |> Enum.map(fn {_bs, sl, _bl} -> sl end)
end)
non_empty_sky_light_sections = sky_light_sections |> Enum.filter(fn section -> !Enum.all?(section, &(&1 == 0)) end)
has_sky_light = [false] ++ (sky_light_sections |> Enum.map(fn section -> !Enum.all?(section, &(&1 == 0)) end)) ++ [false]
empty_sky_light = has_sky_light |> Enum.map(&not/1)
sky_light_array = non_empty_sky_light_sections |> Enum.map(&(Enum.reduce(&1, <<>>, fn light, acc ->
<<acc::bitstring, light::size(4)>>
end)))
block_light_sections = Enum.chunk_every(chunk_array, 16, 16, 0)
|> Enum.map(fn chunk_section ->
chunk_section |> List.flatten() |> Enum.map(fn {_bs, _sl, bl} -> bl end)
end)
non_empty_block_light_sections = block_light_sections |> Enum.filter(fn section -> !Enum.all?(section, &(&1 == 0)) end)
has_block_light = [false] ++ (block_light_sections |> Enum.map(fn section -> !Enum.all?(section, &(&1 == 0)) end)) ++ [false]
empty_block_light = has_block_light |> Enum.map(&not/1)
block_light_array = non_empty_block_light_sections |> Enum.map(&(Enum.reduce(&1, <<>>, fn light, acc ->
<<acc::bitstring, light::size(4)>>
end)))
send(to, {:send_packet, %{
packet_type: :chunk_data_and_update_light,
chunk_x: cx, chunk_z: cz,
heightmaps: heightmaps,
data: data,
block_entities: [],
# TODO: Light
sky_light_mask: Write.varint(0),
block_light_mask: Write.varint(0),
empty_sky_light_mask: Write.varint(0),
empty_block_light_mask: Write.varint(0),
sky_light_arrays: [],
block_light_arrays: []
sky_light_mask: has_sky_light,
block_light_mask: has_block_light,
empty_sky_light_mask: empty_sky_light,
empty_block_light_mask: empty_block_light,
sky_light_arrays: sky_light_array |> Enum.map(&(%{sky_light_array: &1})),
block_light_arrays: block_light_array |> Enum.map(&(%{block_light_array: &1}))
}})
:ok
end
defp long_aligned_bit_string_reduce(values, bpe) do
defp long_aligned_bit_string_reduce(values, bpe, func) do
values |> Enum.reduce(<<>>, fn value, acc ->
next = <<acc::bitstring, value::big-size(bpe)>>
ret = func.(value)
next = <<acc::bitstring, ret::bitstring-size(bpe)>>
# man i hope they dont suddenly change the size of a long
if rem(bit_size(next), 64) + bpe > 64 do
# gotta pad it
@ -227,21 +270,39 @@ defmodule Amethyst.ConnectionHandler do
end
rescue
e ->
if Mix.env() == :dev do
if Application.get_env(:amethyst, :release, false) do
send(self(), {:disconnect, "§cError handling packet #{inspect(id, base: :hex)}:\n#{Exception.format(:error, e, __STACKTRACE__)}"})
Logger.error("Error handling packet with ID #{inspect(id, base: :hex)} in state #{connstate}: #{Exception.format(:error, e, __STACKTRACE__)}")
else
send(self(), {:disconnect, "§cError handling packet #{inspect(id, base: :hex)}:\n#{Exception.format(:error, e, __STACKTRACE__)}"})
Logger.error("Error handling packet with ID #{inspect(id, base: :hex)} in state #{connstate}: #{Exception.format(:error, e, __STACKTRACE__)}")
end
state
end
end
defp send_packet(socket, connstate, packet, version) do
defp send_packet(socket, connstate, packet, version, state) do
try do
data = connstate.serialize(packet, version)
length = byte_size(data) |> Amethyst.Minecraft.Write.varint()
:gen_tcp.send(socket, length <> data)
container = if Map.get(state, :compression) == nil do
# Packet ID is included in data
Write.varint(byte_size(data)) <> data
else
threshold = Map.get(state, :compression, 0)
data_length = byte_size(data)
if data_length >= threshold do
compressed = Write.varint(data_length) <> :zlib.compress(data)
Write.varint(byte_size(compressed)) <> compressed
else
compressed = Write.varint(0) <> data
Write.varint(byte_size(compressed)) <> compressed
end
end
encrypted = if Map.get(state, :encryption_state) == nil do
container
else
Map.get(state, :encryption_state) |> :crypto.crypto_update(container)
end
:gen_tcp.send(socket, encrypted)
rescue
e ->
Logger.error("Error sending packet #{inspect(packet)} in state #{connstate}: #{Exception.format(:error, e, __STACKTRACE__)}")
@ -249,12 +310,12 @@ defmodule Amethyst.ConnectionHandler do
end
end
defp disconnect(socket, reason, connstate, version) do
defp disconnect(socket, reason, connstate, version, state) do
Logger.info("Disconnecting connection #{inspect(socket)}")
Logger.debug("Disconnecting connection #{inspect(socket)}: #{reason}")
case connstate.disconnect(reason) do
nil -> nil
packet -> send_packet(socket, connstate, packet, version)
packet -> send_packet(socket, connstate, packet, version, state)
end
:gen_tcp.close(socket)
end

View File

@ -35,7 +35,7 @@ defmodule Amethyst.ConnectionReceiver do
{:ok, spawn(fn ->
Process.set_label("ConnectionReceiver for #{inspect(socket)}")
{:ok, pid} = Amethyst.ConnectionHandler.start_link(socket, Amethyst.ConnectionState.Handshake, 0)
receive(socket, pid)
receive(socket, pid, nil, nil)
end)}
end
@ -44,41 +44,88 @@ defmodule Amethyst.ConnectionReceiver do
{:ok, spawn_link(fn ->
Process.set_label("ConnectionReceiver for #{inspect(socket)}")
{:ok, pid} = Amethyst.ConnectionHandler.start_link(socket, Amethyst.ConnectionState.Handshake, 0)
receive(socket, pid)
receive(socket, pid, nil, nil)
end)}
end
@spec receive(:gen_tcp.socket(), pid()) :: no_return()
def receive(socket, sender) do
case get_packet(socket) do
@spec receive(:gen_tcp.socket(), pid(), nil | :crypto.crypto_state(), nil | pos_integer()) :: no_return()
def receive(socket, sender, estate, cstate) do
case get_packet(socket, estate, cstate) do
:closed -> send(sender, :closed)
Process.exit(self(), :normal)
{:error, error} -> Logger.error("Error reading packet: #{error}")
{id, data} -> send(sender, {:packet, id, data})
end
receive(socket, sender)
estate = if estate == nil do
# Ask the handler if we have encryption now
send(sender, {:get_encryption, self()})
receive do
nil -> nil
some -> some
end
else estate end
cstate = if cstate == nil do
# Ask the handler if we have encryption now
send(sender, {:get_compression, self()})
receive do
nil -> nil
some -> some
end
else cstate end
receive(socket, sender, estate, cstate)
end
def get_packet(client) do
case get_varint(client, "") do
def get_packet(client, estate, cstate) do
case get_varint(client, "", estate) do
:closed -> :closed
{:error, error} -> {:error, error}
{[length], ""} ->
recv = :gen_tcp.recv(client, length)
case recv do
{:ok, full_packet} -> ({[id], data} = Read.start(full_packet) |> Read.varint() |> Read.stop()
{id, data})
data = :gen_tcp.recv(client, length)
case data do
{:ok, data} ->
# Perform decryption and decompression
decrypted = if estate == nil do
data
else
:crypto.crypto_update(estate, data)
end
full_packet = if cstate == nil do
# Using "without compression" container
decrypted
else
# Using "with compression" container
{[dlength], rest} = Read.start(decrypted) |> Read.varint |> Read.stop
if dlength == 0 do
# Uncompressed data
rest
else
# Compressed data
rest |> :zlib.uncompress()
end
end
{[id], data} = Read.start(full_packet) |> Read.varint |> Read.stop
{id, data}
{:error, :closed} -> :closed
{:error, error} -> {:error, error}
end
end
end
defp get_varint(client, acc) do
defp get_varint(client, acc, estate) do
case :gen_tcp.recv(client, 1) do
{:ok, byte} -> case byte do
{:ok, byte} ->
byte = case estate do
nil -> byte
ds -> :crypto.crypto_update(ds, byte)
end
case byte do
<<0::1, _::7>> -> Read.start(acc <> byte) |> Read.varint() |> Read.stop()
<<1::1, _::7>> -> get_varint(client, acc <> byte)
<<1::1, _::7>> -> get_varint(client, acc <> byte, estate)
end
{:error, :closed} -> :closed
{:error, error} -> {:error, error}

View File

@ -16,6 +16,7 @@
defmodule Amethyst.Minecraft.Write do
import Bitwise
require Logger
@moduledoc """
This module contains functions for writing Minecraft data.
@ -139,6 +140,28 @@ defmodule Amethyst.Minecraft.Write do
def nbt(value) do
Amethyst.NBT.Write.write_net(value)
end
def bitset(list) do
unaligned = Enum.reduce(list, <<>>, &(if &1 do <<1::1, &2::bitstring>> else <<0::1, &2::bitstring>> end))
aligned = if rem(bit_size(unaligned), 64) == 0 do
unaligned
else
<<0::size(64 - rem(bit_size(unaligned), 64)), unaligned::bitstring>>
end
varint(div(byte_size(aligned), 8)) <> aligned
end
def fixed_bitset(list, length) do
unaligned = Enum.reduce(list, <<>>, &(if &1 do <<1::1, &2::bitstring>> else <<0::1, &2::bitstring>> end))
if bit_size(unaligned) != length do
raise ArgumentError, "Fixed bitset is not the correct length"
end
aligned = if rem(bit_size(unaligned), 64) == 0 do
unaligned
else
<<0::size(64 - rem(bit_size(unaligned), 64)), unaligned::bitstring>>
end
aligned
end
end
defmodule Amethyst.Minecraft.Read do
@ -216,6 +239,11 @@ defmodule Amethyst.Minecraft.Read do
{[data | acc], rest, :reversed}
end
def byte_array({acc, data, :reversed}) do
{[length], rest} = start(data) |> varint |> stop
raw({acc, rest, :reversed}, length)
end
@doc """
Reads a varint. `read` tracks the number of bytes read and `nacc` tracks the number being read.
"""
@ -266,4 +294,10 @@ defmodule Amethyst.Minecraft.Read do
def raw({acc, data, :reversed}) do
{[data | acc], "", :reversed}
end
def fixed_bitset({acc, data, :reversed}, length) do
bytes = ceil(length / 8)
<<value::binary-size(bytes), rest::binary>> = data
value = :binary.bin_to_list(value, 0, 1) |> Enum.reverse() |> Enum.take(length) |> Enum.map(& &1 != 0)
{[value | acc], rest, :reversed}
end
end

View File

@ -30,10 +30,6 @@ defmodule Amethyst.Keys do
GenServer.start_link(__MODULE__, bits, name: __MODULE__)
end
def get_priv do
GenServer.call(__MODULE__, :get_priv)
end
def get_pub do
GenServer.call(__MODULE__, :get_pub)
end
@ -52,22 +48,19 @@ defmodule Amethyst.Keys do
rsa_public_key = {:RSAPublicKey, modulus, public_exponent}
Logger.info("Generated RSA keys")
{:ok, {rsa_public_key, rsa_private_key}}
{:ok, {rsa_public_key, rsa_private_key, bits}}
end
@impl true
def handle_call(:get_priv, _from, {pubkey, privkey}) do
{:reply, :public_key.der_encode(:RSAPrivateKey, privkey), {pubkey, privkey}}
def handle_call(:get_pub, _from, {pubkey, privkey, bits}) do
{:SubjectPublicKeyInfo, pk, :not_encrypted} = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, pubkey)
# Logger.debug("#{inspect(pem_encoded, limit: :infinity)}")
{:reply, pk, {pubkey, privkey, bits}}
end
@impl true
def handle_call(:get_pub, _from, {pubkey, privkey}) do
{:reply, :public_key.der_encode(:RSAPublicKey, pubkey), {pubkey, privkey}}
end
@impl true
def handle_call({:decrypt, encrypted}, _from, {pubkey, privkey}) do
def handle_call({:decrypt, encrypted}, _from, {pubkey, privkey, bits}) do
plaintext = :public_key.decrypt_private(encrypted, privkey)
{:reply, plaintext, {pubkey, privkey}}
{:reply, plaintext, {pubkey, privkey, bits}}
end
end

View File

@ -88,11 +88,22 @@ defmodule Amethyst.Game do
mod.accept_teleport(self(), id, refs)
end
@doc """
`chat/3` is called when a player sends a chat message. Note that it is not how slash commands should be handled.
- 'from' is the PID of the player's connection process (see `login/3`).
- 'message' is the chat message sent by the player.
- `state_refs` are your references (see `instantiate/1`)
"""
@callback chat(from :: pid(), message :: binary(), state_refs :: map()) :: :ok
def chat(%{:mod => mod, :refs => refs}, message) do
mod.chat(self(), message, refs)
end
@doc """
The terrain of a specific chunk column. This is automatically used to load chunks for a player.
For now, this data must be formatted as a 3D list, indexed as [y][z][x].
"""
@callback chunk(from :: pid(), {x :: integer(), z :: integer()}, state_refs :: map()) :: [[[pos_integer()]]]
@callback chunk(from :: pid(), {x :: integer(), z :: integer()}, state_refs :: map()) :: [[[{block_state :: pos_integer(), sky_light :: 0..15, block_light :: 0..15}]]]
def chunk(%{:mod => mod, :refs => refs}, pos) do
mod.chunk(self(), pos, refs)
end

26
apps/amethyst/lib/sha1.ex Normal file
View File

@ -0,0 +1,26 @@
# 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.Minecraft.Sha1 do
@moduledoc """
This module implements the awkward non-standard hex digest hash that Minecraft uses
"""
def hash(input) do
# this sucks but so does the fact that mojang makes me do this so it fits
<<number::integer-signed-size(20*8)>> = :crypto.hash(:sha, input)
inspect(number, base: :hex) |> String.replace("0x", "") |> String.downcase()
end
end

View File

@ -203,10 +203,10 @@ defmodule Amethyst.ConnectionState.Configuration do
# https://gist.github.com/WinX64/ab8c7a8df797c273b32d3a3b66522906 minecraft:plains
basic_biome = compound(%{
"effects" => compound(%{
"sky_color" => int(7907327),
"water_fog_color" => int(329011),
"fog_color" => int(12638463),
"water_color" => int(4159204),
"sky_color" => int(7_907_327),
"water_fog_color" => int(329_011),
"fog_color" => int(12_638_463),
"water_color" => int(4_159_204),
"mood_sound" => compound(%{
"tick_delay" => int(6000),
"offset" => float(2.0),
@ -310,6 +310,16 @@ defmodule Amethyst.ConnectionState.Configuration do
chunk_z: div(floor(z), 16)
}})
send(self(), {:set_position, {x, y, z}})
send(self(), {:send_packet, %{packet_type: :player_info_update_add_player,
players: [
%{
uuid: Map.get(state, :uuid),
name: Map.get(state, :name),
properties: Map.get(state, :properties) |>
Enum.map(fn prop -> %{name: prop["name"], value: prop["value"], signature: Map.get(prop, "signature")} end)
}
]
}})
# Begin keepalive loop
# TODO: Put it under some supervisor
me = self()

View File

@ -60,10 +60,20 @@ defmodule Amethyst.ConnectionState.Login do
Macros.defpacket_serverbound :login_acknowledged, 0x03, 767, []
Macros.defpacket_serverbound :cookie_response, 0x04, 767, [identifier: :string, payload: {:optional, :byte_array}]
def handle(%{packet_type: :login_start, name: name, player_uuid: player_uuid}, 767, _state) do
def handle(%{packet_type: :login_start, name: name, player_uuid: player_uuid}, 767, state) do
Logger.debug("Received login start for #{name} with UUID #{player_uuid}")
if Application.fetch_env!(:amethyst, :encryption) do
raise RuntimeError, "Encryption is not currently supported"
if Application.get_env(:amethyst, :encryption, true) do
verify_token = :crypto.strong_rand_bytes(4)
public_key = Amethyst.Keys.get_pub()
Logger.debug("Public key: #{inspect(public_key, limit: :infinity)}")
send(self(), {:send_packet, %{
packet_type: :encryption_request,
server_id: "",
public_key: public_key,
verify_token: verify_token,
should_authenticate: Application.get_env(:amethyst, :auth, false)
}})
state |> Map.put(:verify_token, verify_token) |> Map.put(:name, name) |> Map.put(:uuid, player_uuid)
else
send(self(), {:send_packet, %{
packet_type: :login_success,
@ -75,7 +85,57 @@ defmodule Amethyst.ConnectionState.Login do
:ok
end
end
def handle(%{packet_type: :encryption_response, shared_secret: secret, verify_token: verify_token}, 767, state) do
secret = Amethyst.Keys.decrypt(secret)
verify_token = Amethyst.Keys.decrypt(verify_token)
if verify_token == Map.get(state, :verify_token, :never) do
send(self(), {:set_encryption, secret})
if Application.get_env(:amethyst, :compression, nil) != nil do
threshold = Application.get_env(:amethyst, :compression, 0)
send(self(), {:send_packet, %{packet_type: :set_compression, threshold: threshold}})
send(self(), {:set_compression, threshold})
end
if Application.get_env(:amethyst, :auth, false) == false do
# Don't check authentication
send(self(), {:send_packet, %{
packet_type: :login_success,
uuid: Map.get(state, :uuid),
username: Map.get(state, :name),
properties: [],
strict_error_handling: true
}})
Map.put(state, :authenticated, false)
else
# Check authentication
pubkey = Amethyst.Keys.get_pub()
hash = Amethyst.Minecraft.Sha1.hash(secret <> pubkey)
url = Application.get_env(:amethyst, :session_server, "https://sessionserver.mojang.com") <> "/session/minecraft/hasJoined?username=" <>
Map.get(state, :name) <> "&serverId=" <> hash # I don't think we need to verify the IP in the use case of Amethyst...
response = Req.get!(url,
headers: [
{"user-agent", "Amethyst/1.0"}
]).body
<<c1::binary-size(8), c2::binary-size(4), c3::binary-size(4), c4::binary-size(4), c5::binary>> = response["id"]
uuid = [c1, c2, c3, c4, c5] |> Enum.join("-")
send(self(), {:send_packet, %{
packet_type: :login_success,
uuid: uuid,
username: response["name"],
properties: response["properties"] |>
Enum.map(fn prop -> %{name: prop["name"], value: prop["value"], signature: Map.get(prop, "signature")} end),
strict_error_handling: true
}})
Map.put(state, :authenticated, true) |> Map.put(:uuid, uuid) |> Map.put(:name, response["name"]) |> Map.put(:properties, response["properties"])
end
else
raise RuntimeError, "Invalid verify token. Broken encryption?"
end
end
def handle(%{packet_type: :login_acknowledged}, 767, _state) do
Logger.debug("Received login acknowledged")
send(self(), {:set_state, Amethyst.ConnectionState.Configuration})

View File

@ -14,7 +14,12 @@
# 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/>.
# TODO!!!: REDO THIS WHOLE THING AGAIN IT'S A MESS
defmodule Amethyst.ConnectionState.Macros do
@moduledoc """
Useful macros for defining packets.
"""
require Logger
defmacro defpacket_serverbound(name, id, version, signature, where \\ true) do
quote do
@ -28,7 +33,8 @@ defmodule Amethyst.ConnectionState.Macros do
defmacro defpacket_clientbound(name, id, version, signature, where \\ true) do
quote do
def serialize(%{packet_type: unquote(name)} = packet, unquote(version)) when unquote(where) do
if Amethyst.ConnectionState.Macros.check_type(packet, unquote(signature)) do
# Don't check types if we are in release mode
if Application.get_env(:amethyst, :release, false) || Amethyst.ConnectionState.Macros.check_type(packet, unquote(signature)) do
Amethyst.Minecraft.Write.varint(unquote(id)) <> Amethyst.ConnectionState.Macros.write_signature(packet, unquote(signature))
else
raise "Invalid packet type for #{unquote(name)}! Got #{inspect(packet)}"
@ -52,6 +58,13 @@ defmodule Amethyst.ConnectionState.Macros do
else
{[nil | acc], rest, :reversed}
end
{:optional, {:raw, length}} ->
{[exists], rest} = Read.start(rest) |> Read.bool() |> Read.stop()
if exists do
Read.raw({acc, rest, :reversed}, length)
else
{[nil | acc], rest, :reversed}
end
{:optional, t} ->
{[exists], rest} = Read.start(rest) |> Read.bool() |> Read.stop()
if exists do
@ -59,6 +72,10 @@ defmodule Amethyst.ConnectionState.Macros do
else
{[nil | acc], rest, :reversed}
end
{:fixed_bitset, length} ->
Read.fixed_bitset({acc, rest, :reversed}, length)
{:raw, length} ->
Read.raw({acc, rest, :reversed}, length)
{:array, signature} ->
{[count], rest} = Read.start(rest) |> Read.varint() |> Read.stop()
if count == 0 do
@ -95,7 +112,7 @@ defmodule Amethyst.ConnectionState.Macros do
Enum.reduce(Map.get(packet, name), "", fn item, acc ->
acc <> write_signature(item, signature)
end)
{:literal, {type, value}} -> acc <> apply(Write, type, [value])
{:literal, type, value} -> acc <> apply(Write, type, [value])
t -> acc <> apply(Write, t, [Map.get(packet, name)])
end
end)
@ -106,7 +123,7 @@ defmodule Amethyst.ConnectionState.Macros do
Enum.all?(signature, fn {name, type} ->
case Map.get(packet, name, :missing) do
:missing ->
if elem(type, 0) == :literal do
if is_tuple(type) && elem(type, 0) == :literal do
true
else
throw {:missing, name}
@ -127,22 +144,22 @@ defmodule Amethyst.ConnectionState.Macros do
def type_matches(value, :bool) when is_boolean(value), do: true
def type_matches(value, :byte) when is_integer(value) and value in -128..127, do: true
def type_matches(value, :ubyte) when is_integer(value) and value in 0..255, do: true
def type_matches(value, :short) when is_integer(value) and value in -32768..32767, do: true
def type_matches(value, :ushort) when is_integer(value) and value in 0..65535, do: true
def type_matches(value, :int) when is_integer(value) and value in -2147483648..2147483647, do: true
def type_matches(value, :long) when is_integer(value) and value in -9223372036854775808..9223372036854775807, do: true
def type_matches(value, :short) when is_integer(value) and value in -32_768..32_767, do: true
def type_matches(value, :ushort) when is_integer(value) and value in 0..65_535, do: true
def type_matches(value, :int) when is_integer(value) and value in -2_147_483_648..2_147_483_647, do: true
def type_matches(value, :long) when is_integer(value) and value in -9_223_372_036_854_775_808..9_223_372_036_854_775_807, do: true
def type_matches(value, :float) when is_number(value), do: true
def type_matches(value, :double) when is_number(value), do: true
def type_matches(value, :varint) when is_integer(value) and value in -2147483648..2147483647, do: true
def type_matches(value, :varlong) when is_integer(value) and value in -9223372036854775808..9223372036854775807, do: true
def type_matches(value, :varint) when is_integer(value) and value in -2_147_483_648..2_147_483_647, do: true
def type_matches(value, :varlong) when is_integer(value) and value in -9_223_372_036_854_775_808..9_223_372_036_854_775_807, do: true
def type_matches(value, :uuid) when is_binary(value) and byte_size(value) == 36, do: true
def type_matches(value, :string) when is_binary(value), do: true
def type_matches(value, :raw) when is_binary(value), do: true
def type_matches(value, :byte_array) when is_binary(value), do: true
def type_matches({x, y, z}, :position) when
is_integer(x) and x in -33554432..33554431 and
is_integer(x) and x in -33_554_432..33_554_431 and
is_integer(y) and y in -2048..2047 and
is_integer(z) and z in -33554432..33554431, do: true
is_integer(z) and z in -33_554_432..33_554_431, do: true
def type_matches(value, :nbt), do: Amethyst.NBT.Write.check_type(value)
def type_matches(value, :json) do
case Jason.encode(value) do
@ -153,6 +170,7 @@ defmodule Amethyst.ConnectionState.Macros do
def type_matches(value, {:optional, _type}) when is_nil(value), do: true
def type_matches(value, {:optional, type}), do: type_matches(value, type)
def type_matches(value, {:array, signature}) when is_list(value), do: Enum.all?(value, fn item -> check_type(item, signature) end)
def type_matches(value, :bitset) when is_list(value), do: Enum.all?(value, fn item -> is_boolean(item) end)
def type_matches(value, {:compound, signature}) when is_map(value), do: check_type(value, signature)
def type_matches(_, _) do
false

View File

@ -36,10 +36,10 @@ defmodule Amethyst.ConnectionState.Play do
type: :varint,
data: :nbt
]},
sky_light_mask: :raw,
block_light_mask: :raw,
empty_sky_light_mask: :raw,
empty_block_light_mask: :raw,
sky_light_mask: :bitset,
block_light_mask: :bitset,
empty_sky_light_mask: :bitset,
empty_block_light_mask: :bitset,
sky_light_arrays: {:array, [
sky_light_array: :byte_array
]},
@ -71,8 +71,8 @@ defmodule Amethyst.ConnectionState.Play do
portal_cooldown: :varint,
enforces_secure_chat: :bool,
]
Macros.defpacket_clientbound :player_info_update_add_player, 0x2E, 767, [
actions: {:literal, 0x01},
Macros.defpacket_clientbound :player_info_update_add_player, 0x3E, 767, [
actions: {:literal, :byte, 0x01},
players: {:array, [
uuid: :uuid,
name: :string,
@ -83,8 +83,8 @@ defmodule Amethyst.ConnectionState.Play do
]}
]}
]
Macros.defpacket_clientbound :player_info_update_initialize_chat, 0x2E, 767, [
actions: {:literal, 0x02},
Macros.defpacket_clientbound :player_info_update_initialize_chat, 0x3E, 767, [
actions: {:literal, :byte, 0x02},
players: {:array, [
uuid: :uuid,
data: {:optional, {:compound, [
@ -95,29 +95,29 @@ defmodule Amethyst.ConnectionState.Play do
]}}
]}
]
Macros.defpacket_clientbound :player_info_update_update_game_mode, 0x2E, 767, [
actions: {:literal, 0x04},
Macros.defpacket_clientbound :player_info_update_update_game_mode, 0x3E, 767, [
actions: {:literal, :byte, 0x04},
players: {:array, [
uuid: :uuid,
gamemode: :varint
]}
]
Macros.defpacket_clientbound :player_info_update_update_listed, 0x2E, 767, [
actions: {:literal, 0x08},
Macros.defpacket_clientbound :player_info_update_update_listed, 0x3E, 767, [
actions: {:literal, :byte, 0x08},
players: {:array, [
uuid: :uuid,
listed: :bool
]}
]
Macros.defpacket_clientbound :player_info_update_update_latency, 0x2E, 767, [
actions: {:literal, 0x10},
Macros.defpacket_clientbound :player_info_update_update_latency, 0x3E, 767, [
actions: {:literal, :byte, 0x10},
players: {:array, [
uuid: :uuid,
ping: :varint, # Milliseconds
]}
]
Macros.defpacket_clientbound :player_info_update_update_display_name, 0x2E, 767, [
actions: {:literal, 0x20},
Macros.defpacket_clientbound :player_info_update_update_display_name, 0x3E, 767, [
actions: {:literal, :byte, 0x20},
players: {:array, [
uuid: :uuid,
display_name: {:optional, :nbt}
@ -135,6 +135,10 @@ defmodule Amethyst.ConnectionState.Play do
Macros.defpacket_clientbound :set_center_chunk, 0x54, 767, [
chunk_x: :varint, chunk_z: :varint
]
Macros.defpacket_clientbound :system_chat_message, 0x6C, 767, [
content: :nbt,
overlay: :bool
]
Macros.defpacket_clientbound :game_event, 0x22, 767, [
event: :ubyte, value: :float
@ -157,6 +161,14 @@ defmodule Amethyst.ConnectionState.Play do
def ge_start_waiting_for_level_chunks(767), do: %{packet_type: :game_event, event: 13, value: 0}
Macros.defpacket_serverbound :confirm_teleportation, 0x00, 767, [teleport_id: :varint]
Macros.defpacket_serverbound :chat_message, 0x06, 767, [
message: :string,
timestamp: :long,
salt: :long,
signature: {:optional, {:raw, 256}},
message_count: :varint,
acknowledged: {:fixed_bitset, 20}
]
Macros.defpacket_serverbound :serverbound_plugin_message, 0x12, 767, [channel: :string, data: :raw]
Macros.defpacket_serverbound :keep_alive, 0x18, 767, [id: :long]
Macros.defpacket_serverbound :set_player_position, 0x1A, 767, [
@ -233,6 +245,12 @@ defmodule Amethyst.ConnectionState.Play do
end
end
def handle(%{packet_type: :chat_message, message: msg, timestamp: _, salt: _, signature: _, message_count: _, acknowledged: _}, 767, state) do
# We will never support message signing
state |> Map.get(:game) |> Amethyst.Game.chat(msg)
:ok
end
def handle(%{packet_type: :keep_alive, id: id}, 767, state) do
ka = state |> Map.get(:keepalive)
send(ka, {:respond, id})

View File

@ -39,7 +39,8 @@ defmodule Amethyst.MixProject do
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:uuid, "~> 1.1"},
{:ex_doc, "~> 0.22", only: :dev, runtime: false},
{:jason, "~> 1.4"}
{:jason, "~> 1.4"},
{:req, "~> 0.5.6", runtime: true}
]
end
end

View File

@ -0,0 +1,10 @@
defmodule Sha1Test do
alias Amethyst.Minecraft.Sha1
use ExUnit.Case, async: true
test "Mojang sha1 hash digest" do
assert Sha1.hash("Notch") == "4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48"
assert Sha1.hash("jeb_") == "-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1"
assert Sha1.hash("simon") == "88e16a1019277b15d58faf0541e11910eb756f6"
end
end

View File

@ -2,6 +2,12 @@ defmodule Example.Game do
require Logger
@behaviour Amethyst.Game
@moduledoc """
Example game used for testing Amethyst.
"""
require Amethyst.NBT.Write
alias Amethyst.NBT
@impl true
def instantiate(supervisor) do
Logger.info("The supervisor for this game is at #{inspect(supervisor)}")
@ -35,21 +41,36 @@ defmodule Example.Game do
:ok
end
@impl true
def chat(from, message, _state_refs) do
Logger.info("Player at #{inspect(from)} said: #{inspect(message)}")
send(from, {:send_packet, %{
packet_type: :system_chat_message,
content: NBT.Write.compound(%{"text" => NBT.Write.string("You said: #{message}")}),
overlay: false
}})
:ok
end
@impl true
def joinable?(_refs) do
true
end
@impl true
def chunk(_from, {_cx, _cz}, _state_refs) do
def chunk(_from, {cx, cz}, _state_refs) do
# Logger.info("Player at #{inspect(from)} wants to know chunk #{cx}, #{cz}")
(0..255) |> Enum.map(fn y ->
(0..15) |> Enum.map(fn z ->
(0..15) |> Enum.map(fn x ->
if y <= x + z do
3
gx = cx * 16 + x
gz = cz * 16 + z
gy = y
if rem(gx, 4) == 0 && rem(gy, 4) == 0 && rem(gz, 4) == 0 do
{abs(rem(div(gx + gy + gz, 4), 300)), 7, 7}
else
0
{0, 7, 7}
end
end)
end)

View File

@ -2,6 +2,9 @@ import Config
config :amethyst,
port: 25599, # Bogus port for testing, avoids unexpected conflicts
encryption: false, # Whether or not to request encryption from clients.
auth: false, # Whether or not users should be authenticated with Mojang.
default_game: Example.Game # Which game new players should be sent to
encryption: true, # Whether or not to request encryption from clients.
auth: true, # Whether or not users should be authenticated with Mojang.
session_server: "https://sessionserver.mojang.com", # Base URL to the Mojang session server
compression: 256, # Packets larger than this amount are sent compressed. Set to nil to disable compression.
default_game: Example.Game, # Which game new players should be sent to
release: config_env() == :prod # If this is set to false, Amethyst will perform additional checks at runtime and will handle errors more loosely

View File

@ -43,14 +43,20 @@
})
);
#secrets = import ./secrets.nix;
package = env: pkgs: (pkgs.beamPackages.mixRelease {
package = env: release: pkgs: (pkgs.beamPackages.mixRelease {
pname = "amethyst";
version = "0.1.0";
src = ./.;
elixir = pkgs.elixir;
mixEnv = env;
mixReleaseName = release;
# This should just be part of mixRelease, I don't know why we need to do it here
installPhase = ''
runHook preInstall
mix release ${release} --no-deps-check --path "$out"
runHook postInstall
'';
meta.mainProgram = "${release}";
erlangDeterministicBuilds = false; # Technically less pure but allows doctests
removeCookie = false; # Insecure; Access to the file system can allow the cookie to be read and provides remote control of the Erlang VM
mixNixDeps = import ./mix.nix {inherit lib; beamPackages = pkgs.beamPackages;};
@ -64,8 +70,9 @@
in {
packages = eachSystem (pkgs:
{
default = package "prod" pkgs;
dev = package "dev" pkgs;
default = package "prod" "amethyst" pkgs;
dev = package "dev" "amethyst" pkgs;
example = package "prop" "example" pkgs;
}
);

View File

@ -8,7 +8,7 @@ defmodule AmethystUmbrella.MixProject do
elixir: "~> 1.17",
start_permanent: Mix.env() == :prod,
deps: deps(),
default_release: :amethyst,
# default_release: :amethyst,
releases: [
amethyst: [

View File

@ -5,11 +5,19 @@
"elixir_math": {:hex, :elixir_math, "0.1.2", "5655bdf7f34e30906f31cdcf3031b43dd522ce8d2936b60ad4696b2c752bf5c9", [:mix], [], "hexpm", "34f4e4384903097a8ec566784fa8e9aa2b741247d225741f07cc48250c2aa64c"},
"ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"},
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
"hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"pre_commit": {:hex, :pre_commit, "0.3.4", "e2850f80be8090d50ad8019ef2426039307ff5dfbe70c736ad0d4d401facf304", [:mix], [], "hexpm", "16f684ba4f1fed1cba6b19e082b0f8d696e6f1c679285fedf442296617ba5f4e"},
"req": {:hex, :req, "0.5.6", "8fe1eead4a085510fe3d51ad854ca8f20a622aae46e97b302f499dfb84f726ac", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cfaa8e720945d46654853de39d368f40362c2641c4b2153c886418914b372185"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"},
}

130
mix.nix
View File

@ -47,6 +47,19 @@ let
beamDeps = [];
};
elixir_math = buildMix rec {
name = "elixir_math";
version = "0.1.2";
src = fetchHex {
pkg = "elixir_math";
version = "${version}";
sha256 = "34f4e4384903097a8ec566784fa8e9aa2b741247d225741f07cc48250c2aa64c";
};
beamDeps = [];
};
ex_doc = buildMix rec {
name = "ex_doc";
version = "0.34.2";
@ -73,6 +86,32 @@ let
beamDeps = [];
};
finch = buildMix rec {
name = "finch";
version = "0.19.0";
src = fetchHex {
pkg = "finch";
version = "${version}";
sha256 = "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6";
};
beamDeps = [ mime mint nimble_options nimble_pool telemetry ];
};
hpax = buildMix rec {
name = "hpax";
version = "1.0.0";
src = fetchHex {
pkg = "hpax";
version = "${version}";
sha256 = "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601";
};
beamDeps = [];
};
jason = buildMix rec {
name = "jason";
version = "1.4.4";
@ -125,6 +164,45 @@ let
beamDeps = [ makeup ];
};
mime = buildMix rec {
name = "mime";
version = "2.0.6";
src = fetchHex {
pkg = "mime";
version = "${version}";
sha256 = "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e";
};
beamDeps = [];
};
mint = buildMix rec {
name = "mint";
version = "1.6.2";
src = fetchHex {
pkg = "mint";
version = "${version}";
sha256 = "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9";
};
beamDeps = [ hpax ];
};
nimble_options = buildMix rec {
name = "nimble_options";
version = "1.1.1";
src = fetchHex {
pkg = "nimble_options";
version = "${version}";
sha256 = "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44";
};
beamDeps = [];
};
nimble_parsec = buildMix rec {
name = "nimble_parsec";
version = "1.4.0";
@ -138,6 +216,58 @@ let
beamDeps = [];
};
nimble_pool = buildMix rec {
name = "nimble_pool";
version = "1.1.0";
src = fetchHex {
pkg = "nimble_pool";
version = "${version}";
sha256 = "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a";
};
beamDeps = [];
};
pre_commit = buildMix rec {
name = "pre_commit";
version = "0.3.4";
src = fetchHex {
pkg = "pre_commit";
version = "${version}";
sha256 = "16f684ba4f1fed1cba6b19e082b0f8d696e6f1c679285fedf442296617ba5f4e";
};
beamDeps = [];
};
req = buildMix rec {
name = "req";
version = "0.5.6";
src = fetchHex {
pkg = "req";
version = "${version}";
sha256 = "cfaa8e720945d46654853de39d368f40362c2641c4b2153c886418914b372185";
};
beamDeps = [ finch jason mime ];
};
telemetry = buildRebar3 rec {
name = "telemetry";
version = "1.3.0";
src = fetchHex {
pkg = "telemetry";
version = "${version}";
sha256 = "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6";
};
beamDeps = [];
};
uuid = buildMix rec {
name = "uuid";
version = "1.1.8";