├── .formatter.exs ├── .gitignore ├── Dockerfile ├── README.md ├── apps ├── isl │ ├── .formatter.exs │ ├── .gitignore │ ├── lib │ │ └── isl │ │ │ ├── application.ex │ │ │ ├── cipher.ex │ │ │ └── connection.ex │ ├── mix.exs │ ├── mix.lock │ ├── rel │ │ ├── env.bat.eex │ │ ├── env.sh.eex │ │ ├── remote.vm.args.eex │ │ └── vm.args.eex │ └── test │ │ ├── isl │ │ └── cipher_test.exs │ │ ├── isl_test.exs │ │ └── test_helper.exs ├── line_reversal │ ├── .formatter.exs │ ├── .gitignore │ ├── lib │ │ └── line_reversal │ │ │ ├── acceptor.ex │ │ │ ├── application.ex │ │ │ ├── connection.ex │ │ │ ├── lrcp.ex │ │ │ └── lrcp │ │ │ ├── listen_socket.ex │ │ │ ├── protocol.ex │ │ │ └── socket.ex │ ├── mix.exs │ ├── mix.lock │ ├── rel │ │ ├── env.bat.eex │ │ ├── env.sh.eex │ │ ├── remote.vm.args.eex │ │ └── vm.args.eex │ └── test │ │ ├── line_reversal │ │ ├── lrcp │ │ │ └── protocol_test.exs │ │ └── udp_server_test.exs │ │ └── test_helper.exs ├── protohackers_first_days │ ├── lib │ │ └── protohackers │ │ │ ├── application.ex │ │ │ ├── budget_chat_server.ex │ │ │ ├── echo_server.ex │ │ │ ├── mitm │ │ │ ├── acceptor.ex │ │ │ ├── boguscoin.ex │ │ │ ├── connection.ex │ │ │ ├── connection_supervisor.ex │ │ │ └── supervisor.ex │ │ │ ├── prices_server.ex │ │ │ ├── prices_server │ │ │ └── db.ex │ │ │ ├── prime_server.ex │ │ │ └── udp_server.ex │ ├── mix.exs │ ├── mix.lock │ ├── rel │ │ ├── env.bat.eex │ │ ├── env.sh.eex │ │ ├── remote.vm.args.eex │ │ └── vm.args.eex │ └── test │ │ ├── protohackers │ │ ├── budget_chat_server_test.exs │ │ ├── echo_server_test.exs │ │ ├── mitm │ │ │ └── boguscoin_test.exs │ │ ├── prices_server │ │ │ └── db_test.exs │ │ ├── prices_server_test.exs │ │ ├── prime_server_test.exs │ │ └── udp_server_test.exs │ │ └── test_helper.exs └── speed_daemon │ ├── .formatter.exs │ ├── .gitignore │ ├── lib │ └── speed_daemon │ │ ├── acceptor.ex │ │ ├── application.ex │ │ ├── central_ticket_dispatcher.ex │ │ ├── connection.ex │ │ ├── connection_supervisor.ex │ │ ├── message.ex │ │ └── supervisor.ex │ ├── mix.exs │ ├── mix.lock │ ├── rel │ ├── env.bat.eex │ ├── env.sh.eex │ ├── remote.vm.args.eex │ └── vm.args.eex │ └── test │ ├── speed_daemon │ ├── integration_test.exs │ └── message_test.exs │ └── test_helper.exs ├── config ├── config.exs └── runtime.exs ├── fly.toml ├── mix.exs └── mix.lock /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | /apps/*/_build 4 | 5 | # If you run "mix test --cover", coverage assets end up here. 6 | /cover/ 7 | 8 | # The directory Mix downloads your dependencies sources to. 9 | /deps/ 10 | /apps/*/deps 11 | 12 | # Where third-party dependencies like ExDoc output generated docs. 13 | /doc/ 14 | 15 | # Ignore .fetch files in case you like to edit your project deps locally. 16 | /.fetch 17 | 18 | # If the VM crashes, it generates a dump, let's ignore it too. 19 | erl_crash.dump 20 | 21 | # Also ignore archive artifacts (built via "mix archive.build"). 22 | *.ez 23 | 24 | # Ignore package tarball (built via "mix hex.build"). 25 | protohackers-*.tar 26 | 27 | # Temporary files, for example, from tests. 28 | /tmp/ 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BUILDER_IMAGE="hexpm/elixir:1.14.2-erlang-25.0.4-debian-bullseye-20220801-slim" 2 | ARG RUNNER_IMAGE="debian:bullseye-20220801-slim" 3 | 4 | FROM ${BUILDER_IMAGE} AS builder 5 | 6 | ARG APPLICATION="isl" 7 | 8 | # Set env variables 9 | ENV MIX_ENV="prod" 10 | 11 | # Install build dependencies 12 | RUN apt-get update -y && apt-get install -y build-essential git \ 13 | && apt-get clean && rm -f /var/lib/apt/lists/*_* 14 | 15 | WORKDIR /app 16 | 17 | # Install Hex and rebar3 18 | RUN mix do local.hex --force, local.rebar --force 19 | 20 | # Copy configuration from this app and all children 21 | COPY config config 22 | 23 | # Copy mix.exs and mix.lock from all children applications 24 | COPY mix.exs ./ 25 | COPY apps/${APPLICATION}/mix.exs apps/${APPLICATION}/mix.exs 26 | COPY apps/${APPLICATION}/mix.lock apps/${APPLICATION}/mix.lock 27 | RUN mix do deps.get --only $MIX_ENV, deps.compile 28 | 29 | # Copy lib for all applications and compile 30 | COPY apps/${APPLICATION}/lib apps/${APPLICATION}/lib 31 | RUN mix compile 32 | 33 | # Changes to config/runtime.exs don't require recompiling the code 34 | COPY apps/${APPLICATION}/rel apps/${APPLICATION}/rel 35 | RUN mix release ${APPLICATION} 36 | 37 | ## Runner image 38 | 39 | FROM ${RUNNER_IMAGE} 40 | 41 | ENV APPLICATION="isl" 42 | 43 | # Install dependencies 44 | RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales \ 45 | && apt-get clean && rm -f /var/lib/apt/lists/*_* 46 | 47 | # Set the locale 48 | RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen 49 | ENV LANG en_US.UTF-8 50 | ENV LANGUAGE en_US:en 51 | ENV LC_ALL en_US.UTF-8 52 | 53 | WORKDIR /app 54 | 55 | COPY --from=builder /app/_build/prod/rel ./ 56 | 57 | CMD /app/${APPLICATION}/bin/${APPLICATION} start 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Protohackers in Elixir 2 | 3 | This repository contains the code for the video series I'm making, focused on 4 | solving the [Protohackers] network challenges in Elixir. 5 | 6 | You can find the video series on [my YouTube channel][youtube-channel], starting 7 | from the first video: 8 | 9 | 10 | Thumbnail for the first video, which is an AI-generated drawing of an otter sitting at a desk and programming 11 | 12 | 13 | ## Running Locally 14 | 15 | You can run this application locally by cloning the repository and running: 16 | 17 | ```shell 18 | mix deps.get 19 | mix run --no-halt 20 | ``` 21 | 22 | ## Deploying 23 | 24 | I'm deploying this application on [Fly.io][fly]. 25 | 26 | [Protohackers]: https://protohackers.com 27 | [youtube-channel]: https://www.youtube.com/channel/UCiaFBwlunX1m8FKwZQ1GOSA 28 | [fly]: https://fly.io 29 | -------------------------------------------------------------------------------- /apps/isl/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /apps/isl/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | isl-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /apps/isl/lib/isl/application.ex: -------------------------------------------------------------------------------- 1 | defmodule ISL.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | port = String.to_integer(System.get_env("TCP_PORT", "5009")) 11 | 12 | children = [ 13 | {ThousandIsland, port: port, handler_module: ISL.Connection} 14 | ] 15 | 16 | # See https://hexdocs.pm/elixir/Supervisor.html 17 | # for other strategies and supported options 18 | opts = [strategy: :one_for_one, name: Isl.Supervisor] 19 | Supervisor.start_link(children, opts) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /apps/isl/lib/isl/cipher.ex: -------------------------------------------------------------------------------- 1 | defmodule ISL.Cipher do 2 | @type spec() :: [ 3 | :reversebits 4 | | {:xor, byte()} 5 | | :xorpos 6 | | {:add, byte()} 7 | | :addpos 8 | | {:sub, byte()} 9 | | :subpos 10 | ] 11 | 12 | import Bitwise 13 | 14 | require Integer 15 | 16 | @doc """ 17 | ## Examples 18 | 19 | iex> ISL.Cipher.parse_spec(<<0x00>>) 20 | {:ok, [], <<>>} 21 | 22 | iex> ISL.Cipher.parse_spec(<<0x00, "hello">>) 23 | {:ok, [], "hello"} 24 | 25 | iex> ISL.Cipher.parse_spec(<<0x02, 0xaa, 0x01, 0x00, "hello">>) 26 | {:ok, [{:xor, 0xaa}, :reversebits], "hello"} 27 | 28 | iex> ISL.Cipher.parse_spec(<<0xff>>) 29 | :error 30 | 31 | iex> ISL.Cipher.parse_spec(<<0x01>>) 32 | :error 33 | 34 | """ 35 | @spec parse_spec(binary()) :: {:ok, spec(), binary()} | :error 36 | def parse_spec(binary) when is_binary(binary) do 37 | parse_spec(binary, _acc = []) 38 | end 39 | 40 | defp parse_spec(<<0x00, rest::binary>>, acc), do: {:ok, Enum.reverse(acc), rest} 41 | defp parse_spec(<<0x01, rest::binary>>, acc), do: parse_spec(rest, [:reversebits | acc]) 42 | defp parse_spec(<<0x02, n, rest::binary>>, acc), do: parse_spec(rest, [{:xor, n} | acc]) 43 | defp parse_spec(<<0x03, rest::binary>>, acc), do: parse_spec(rest, [:xorpos | acc]) 44 | defp parse_spec(<<0x04, n, rest::binary>>, acc), do: parse_spec(rest, [{:add, n} | acc]) 45 | defp parse_spec(<<0x05, rest::binary>>, acc), do: parse_spec(rest, [:addpos | acc]) 46 | defp parse_spec(_other, _acc), do: :error 47 | 48 | @doc """ 49 | 50 | iex> ISL.Cipher.apply("hello", [{:xor, 1}, :reversebits], 0) 51 | <<0x96, 0x26, 0xb6, 0xb6, 0x76>> 52 | 53 | """ 54 | @spec apply(binary(), spec(), non_neg_integer()) :: binary() 55 | def apply(data, spec, start_position) do 56 | {encoded, _position} = 57 | for <>, reduce: {_acc = <<>>, start_position} do 58 | {acc, position} -> 59 | encoded = Enum.reduce(spec, byte, &apply_operation(&1, &2, position)) 60 | {<>, position + 1} 61 | end 62 | 63 | encoded 64 | end 65 | 66 | defp apply_operation(:reversebits, byte, _position), do: reverse_bits(byte) 67 | defp apply_operation({:xor, n}, byte, _position), do: bxor(n, byte) 68 | defp apply_operation(:xorpos, byte, position), do: bxor(byte, position) 69 | defp apply_operation({:add, n}, byte, _position), do: rem(byte + n, 256) 70 | defp apply_operation(:addpos, byte, position), do: rem(byte + position, 256) 71 | defp apply_operation({:sub, n}, byte, _position), do: rem(byte - n, 256) 72 | defp apply_operation(:subpos, byte, position), do: rem(byte - position, 256) 73 | 74 | @doc """ 75 | 76 | iex> ISL.Cipher.reverse_bits(0b00000001) 77 | 0b10000000 78 | 79 | """ 80 | @spec reverse_bits(byte()) :: byte() 81 | def reverse_bits(byte) do 82 | <> = <> 83 | <> = <> 84 | reversed 85 | end 86 | 87 | @doc """ 88 | iex> ISL.Cipher.reverse_spec([:reversebits, :xorpos, :addpos, {:xor, 3}, {:add, 9}]) 89 | [{:sub, 9}, {:xor, 3}, :subpos, :xorpos, :reversebits] 90 | """ 91 | @spec reverse_spec(spec()) :: spec() 92 | def reverse_spec(spec) do 93 | spec 94 | |> Enum.reverse() 95 | |> Enum.map(fn 96 | :addpos -> :subpos 97 | {:add, n} -> {:sub, n} 98 | other -> other 99 | end) 100 | end 101 | 102 | @spec no_op?(spec()) :: boolean() 103 | def no_op?(ops) do 104 | {xorpos_ops, ops} = Enum.split_with(ops, &(&1 == :xorpos)) 105 | 106 | {adds, ops} = Enum.split_with(ops, &match?({:add, _}, &1)) 107 | total_add = Enum.reduce(adds, 0, fn {:add, n}, acc -> rem(n + acc, 256) end) 108 | {addpos_ops, ops} = Enum.split_with(ops, &(&1 == :addpos)) 109 | 110 | cond do 111 | Integer.is_odd(length(xorpos_ops)) -> false 112 | addpos_ops != [] -> false 113 | total_add != 0 -> false 114 | true -> no_op_rest?(ops, _xor_acc = 0, _reversed? = false) 115 | end 116 | end 117 | 118 | defp no_op_rest?([], _xor_acc = 0, _reversed? = false), do: true 119 | defp no_op_rest?([], _xor_acc, _reversed?), do: false 120 | 121 | defp no_op_rest?([:reversebits, :reversebits | rest], xor_acc, reversed?) do 122 | no_op_rest?(rest, xor_acc, reversed?) 123 | end 124 | 125 | defp no_op_rest?([:reversebits | [{:xor, _} | _] = rest], xor_acc, reversed?) do 126 | rest 127 | |> Enum.map(fn 128 | {:xor, n} -> {:xor, reverse_bits(n)} 129 | other -> other 130 | end) 131 | |> no_op_rest?(xor_acc, not reversed?) 132 | end 133 | 134 | defp no_op_rest?([{:xor, _} | _] = spec, xor_acc, reversed?) do 135 | {xors, rest} = Enum.split_while(spec, &match?({:xor, _}, &1)) 136 | xor_acc = Enum.reduce(xors, xor_acc, fn {:xor, n}, acc -> bxor(n, acc) end) 137 | no_op_rest?(rest, xor_acc, reversed?) 138 | end 139 | 140 | defp no_op_rest?([:reversebits | rest], xor_acc, reversed?) do 141 | no_op_rest?(rest, xor_acc, not reversed?) 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /apps/isl/lib/isl/connection.ex: -------------------------------------------------------------------------------- 1 | defmodule ISL.Connection do 2 | use ThousandIsland.Handler 3 | 4 | alias ISL.Cipher 5 | 6 | require Logger 7 | 8 | defstruct [ 9 | :cipher_spec, 10 | :reverse_cipher_spec, 11 | buffer: <<>>, 12 | client_position: 0, 13 | server_position: 0 14 | ] 15 | 16 | @impl true 17 | def handle_connection(_socket, _handler_opts = []) do 18 | {:continue, %__MODULE__{}} 19 | end 20 | 21 | @impl true 22 | def handle_data(data, socket, state) do 23 | Logger.debug("<-- #{inspect(data, base: :hex)}") 24 | 25 | case handle_new_data(state, socket, data) do 26 | {:ok, state} -> {:continue, state} 27 | :error -> {:close, state} 28 | end 29 | end 30 | 31 | defp handle_new_data(%__MODULE__{cipher_spec: nil} = state, socket, data) do 32 | state = update_in(state.buffer, &(&1 <> data)) 33 | 34 | case parse_cipher_spec(state) do 35 | {:ok, state, rest} -> handle_new_data(state, socket, rest) 36 | :error -> :error 37 | end 38 | end 39 | 40 | defp handle_new_data(%__MODULE__{} = state, socket, data) do 41 | data = Cipher.apply(data, state.reverse_cipher_spec, state.client_position) 42 | Logger.debug("Handling decoded data at position #{state.client_position}: #{inspect(data)}") 43 | state = update_in(state.client_position, &(&1 + byte_size(data))) 44 | state = update_in(state.buffer, &(&1 <> data)) 45 | handle_new_decoded_data(state, socket) 46 | end 47 | 48 | defp parse_cipher_spec(%__MODULE__{buffer: data, cipher_spec: nil} = state) do 49 | case Cipher.parse_spec(data) do 50 | {:ok, cipher_spec, rest} -> 51 | if Cipher.no_op?(cipher_spec) do 52 | Logger.error("No-op cipher spec") 53 | :error 54 | else 55 | Logger.debug("Parsed cipher: #{inspect(cipher_spec)}") 56 | state = put_in(state.cipher_spec, cipher_spec) 57 | state = put_in(state.reverse_cipher_spec, Cipher.reverse_spec(cipher_spec)) 58 | state = put_in(state.buffer, <<>>) 59 | {:ok, state, rest} 60 | end 61 | 62 | :error -> 63 | :error 64 | end 65 | end 66 | 67 | defp handle_new_decoded_data(%__MODULE__{} = state, socket) do 68 | case String.split(state.buffer, "\n", parts: 2) do 69 | [line, rest] -> 70 | state = put_in(state.buffer, rest) 71 | state = handle_line(state, socket, line) 72 | handle_new_decoded_data(state, socket) 73 | 74 | [_buffer] -> 75 | {:ok, state} 76 | end 77 | end 78 | 79 | defp handle_line(%__MODULE__{} = state, socket, line) do 80 | encoded = 81 | line 82 | |> String.split(",") 83 | |> Enum.max_by(fn toy_spec -> 84 | case Integer.parse(toy_spec) do 85 | {quantity, "x " <> _toy} -> quantity 86 | :error -> raise "invalid packet" 87 | end 88 | end) 89 | |> Kernel.<>("\n") 90 | |> Cipher.apply(state.cipher_spec, state.server_position) 91 | 92 | ThousandIsland.Socket.send(socket, encoded) 93 | update_in(state.server_position, &(&1 + byte_size(encoded))) 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /apps/isl/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ISL.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :isl, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.14", 13 | start_permanent: Mix.env() == :prod, 14 | deps: deps() 15 | ] 16 | end 17 | 18 | # Run "mix help compile.app" to learn about applications. 19 | def application do 20 | [ 21 | extra_applications: [:logger], 22 | mod: {ISL.Application, []} 23 | ] 24 | end 25 | 26 | # Run "mix help deps" to learn about dependencies. 27 | defp deps do 28 | [ 29 | {:thousand_island, "~> 0.6.0"} 30 | ] 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /apps/isl/mix.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatyouhide/protohackers_in_elixir/f73015610479467c0d60fcaeda6da8dbcc4d6538/apps/isl/mix.lock -------------------------------------------------------------------------------- /apps/isl/rel/env.bat.eex: -------------------------------------------------------------------------------- 1 | @echo off 2 | rem Set the release to load code on demand (interactive) instead of preloading (embedded). 3 | rem set RELEASE_MODE=interactive 4 | 5 | rem Set the release to work across nodes. 6 | rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none". 7 | rem set RELEASE_DISTRIBUTION=name 8 | rem set RELEASE_NODE=<%= @release.name %> 9 | -------------------------------------------------------------------------------- /apps/isl/rel/env.sh.eex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ip=$(grep fly-local-6pn /etc/hosts | cut -f 1) 4 | export RELEASE_DISTRIBUTION=name 5 | export RELEASE_NODE=$FLY_APP_NAME@"$ip" 6 | export ELIXIR_ERL_OPTIONS="-proto_dist inet6_tcp" 7 | -------------------------------------------------------------------------------- /apps/isl/rel/remote.vm.args.eex: -------------------------------------------------------------------------------- 1 | ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html 2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here 3 | 4 | ## Increase number of concurrent ports/sockets 5 | ##+Q 65536 6 | 7 | ## Tweak GC to run more often 8 | ##-env ERL_FULLSWEEP_AFTER 10 9 | -------------------------------------------------------------------------------- /apps/isl/rel/vm.args.eex: -------------------------------------------------------------------------------- 1 | ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html 2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here 3 | 4 | ## Increase number of concurrent ports/sockets 5 | ##+Q 65536 6 | 7 | ## Tweak GC to run more often 8 | ##-env ERL_FULLSWEEP_AFTER 10 9 | -------------------------------------------------------------------------------- /apps/isl/test/isl/cipher_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ISL.CipherTest do 2 | use ExUnit.Case, async: true 3 | 4 | doctest ISL.Cipher 5 | end 6 | -------------------------------------------------------------------------------- /apps/isl/test/isl_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ISLTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "example session from the problem description" do 5 | {:ok, client} = :gen_tcp.connect(~c"localhost", 5009, [:binary, active: true]) 6 | 7 | :ok = :gen_tcp.send(client, <<0x02, 0x7B, 0x05, 0x01, 0x00>>) 8 | 9 | :ok = 10 | :gen_tcp.send( 11 | client, 12 | <<0xF2, 0x20, 0xBA, 0x44, 0x18, 0x84, 0xBA, 0xAA, 0xD0, 0x26, 0x44, 0xA4, 0xA8, 0x7E>> 13 | ) 14 | 15 | assert_receive {:tcp, ^client, <<0x72, 0x20, 0xBA, 0xD8, 0x78, 0x70, 0xEE>>}, 500 16 | 17 | :ok = 18 | :gen_tcp.send( 19 | client, 20 | <<0x6A, 0x48, 0xD6, 0x58, 0x34, 0x44, 0xD6, 0x7A, 0x98, 0x4E, 0x0C, 0xCC, 0x94, 0x31>> 21 | ) 22 | 23 | assert_receive {:tcp, ^client, <<0xF2, 0xD0, 0x26, 0xC8, 0xA4, 0xD8, 0x7E>>}, 500 24 | end 25 | 26 | @tag :capture_log 27 | test "no-op ciphers result in the client being disconnected" do 28 | {:ok, client} = :gen_tcp.connect(~c"localhost", 5009, [:binary, active: true]) 29 | 30 | # Cipher spec from the problem description 31 | :ok = :gen_tcp.send(client, <<0x02, 0xA0, 0x02, 0x0B, 0x02, 0xAB, 0x00>>) 32 | 33 | assert_receive {:tcp_closed, ^client} 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /apps/isl/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /apps/line_reversal/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /apps/line_reversal/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | line_reversal-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /apps/line_reversal/lib/line_reversal/acceptor.ex: -------------------------------------------------------------------------------- 1 | defmodule LineReversal.Acceptor do 2 | use Task, restart: :transient 3 | 4 | alias LineReversal.{LRCP, Connection} 5 | 6 | require Logger 7 | 8 | @spec start_link(keyword()) :: {:ok, pid()} 9 | def start_link(options) when is_list(options) do 10 | ip = Keyword.fetch!(options, :ip) 11 | port = Keyword.fetch!(options, :port) 12 | Task.start_link(__MODULE__, :__accept__, [ip, port]) 13 | end 14 | 15 | ## Private 16 | 17 | def __accept__(ip, port) when is_tuple(ip) and is_integer(port) do 18 | case LRCP.listen(ip, port) do 19 | {:ok, listen_socket} -> 20 | Logger.info("Listening for LRCP connections on port #{port}") 21 | loop(listen_socket) 22 | 23 | {:error, reason} -> 24 | raise "failed to start LRCP listen socket on port #{port}: #{inspect(reason)}" 25 | end 26 | end 27 | 28 | defp loop(listen_socket) do 29 | case LRCP.accept(listen_socket) do 30 | {:ok, socket} -> 31 | {:ok, handler} = Connection.start_link(socket) 32 | :ok = LRCP.controlling_process(socket, handler) 33 | loop(listen_socket) 34 | 35 | {:error, reason} -> 36 | raise "failed to accept LRCP connection: #{inspect(reason)}" 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /apps/line_reversal/lib/line_reversal/application.ex: -------------------------------------------------------------------------------- 1 | defmodule LineReversal.Application do 2 | @moduledoc false 3 | 4 | use Application 5 | 6 | @impl true 7 | def start(_type, _args) do 8 | children = [ 9 | {Registry, name: LineReversal.Registry, keys: :unique}, 10 | {LineReversal.Acceptor, 11 | port: String.to_integer(System.get_env("UDP_PORT", "5008")), ip: udp_ip_address()} 12 | ] 13 | 14 | opts = [strategy: :one_for_one, name: LineReversal.Supervisor] 15 | Supervisor.start_link(children, opts) 16 | end 17 | 18 | defp udp_ip_address do 19 | case System.fetch_env("FLY_APP_NAME") do 20 | {:ok, _} -> 21 | {:ok, fly_global_ip} = :inet.getaddr(~c"fly-global-services", :inet) 22 | fly_global_ip 23 | 24 | :error -> 25 | {0, 0, 0, 0} 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /apps/line_reversal/lib/line_reversal/connection.ex: -------------------------------------------------------------------------------- 1 | defmodule LineReversal.Connection do 2 | use GenServer, restart: :temporary 3 | 4 | alias LineReversal.LRCP 5 | 6 | require Logger 7 | 8 | @spec start_link(LRCP.socket()) :: GenServer.on_start() 9 | def start_link(lrcp_socket) do 10 | GenServer.start_link(__MODULE__, lrcp_socket) 11 | end 12 | 13 | ## Callbacks 14 | 15 | defstruct [:socket, buffer: <<>>] 16 | 17 | @impl true 18 | def init(socket) do 19 | Logger.debug("Connection started: #{inspect(socket)}") 20 | {:ok, %__MODULE__{socket: socket}} 21 | end 22 | 23 | @impl true 24 | def handle_info(message, state) 25 | 26 | def handle_info({:lrcp, socket, data}, %__MODULE__{socket: socket} = state) do 27 | Logger.debug("Received LRCP data: #{inspect(data)}") 28 | state = update_in(state.buffer, &(&1 <> data)) 29 | state = handle_new_data(state) 30 | {:noreply, state} 31 | end 32 | 33 | def handle_info({:lrcp_error, socket, reason}, %__MODULE__{socket: socket} = state) do 34 | Logger.error("Closing connection due to error: #{inspect(reason)}") 35 | {:stop, :normal, state} 36 | end 37 | 38 | def handle_info({:lrcp_closed, socket}, %__MODULE__{socket: socket} = state) do 39 | Logger.debug("Connection closed") 40 | {:stop, :normal, state} 41 | end 42 | 43 | ## Helpers 44 | 45 | defp handle_new_data(%__MODULE__{} = state) do 46 | case String.split(state.buffer, "\n", parts: 2) do 47 | [line, rest] -> 48 | LRCP.send(state.socket, String.reverse(line) <> "\n") 49 | handle_new_data(put_in(state.buffer, rest)) 50 | 51 | [_no_line_yet] -> 52 | state 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /apps/line_reversal/lib/line_reversal/lrcp.ex: -------------------------------------------------------------------------------- 1 | defmodule LineReversal.LRCP do 2 | alias LineReversal.LRCP.{ListenSocket, Socket} 3 | 4 | @type listen_socket() :: ListenSocket.t() 5 | @type socket() :: Socket.t() 6 | 7 | @spec listen(:inet.ip_address(), :inet.port_number()) :: 8 | {:ok, listen_socket()} | {:error, term()} 9 | def listen(ip, port) when is_tuple(ip) and is_integer(port) do 10 | ListenSocket.start_link(ip: ip, port: port) 11 | end 12 | 13 | @spec accept(listen_socket()) :: {:ok, socket()} | {:error, term()} 14 | def accept(%ListenSocket{} = listen_socket) do 15 | ListenSocket.accept(listen_socket) 16 | end 17 | 18 | @spec controlling_process(socket(), pid()) :: :ok | {:error, term()} 19 | def controlling_process(%Socket{} = socket, pid) when is_pid(pid) do 20 | Socket.controlling_process(socket, pid) 21 | end 22 | 23 | @spec send(socket(), binary()) :: :ok | {:error, term()} 24 | def send(%Socket{} = socket, data) when is_binary(data) do 25 | Socket.send(socket, data) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /apps/line_reversal/lib/line_reversal/lrcp/listen_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule LineReversal.LRCP.ListenSocket do 2 | use GenServer 3 | 4 | alias LineReversal.LRCP 5 | 6 | require Logger 7 | 8 | @type t() :: %__MODULE__{pid: pid()} 9 | 10 | defstruct [:pid] 11 | 12 | @spec start_link(keyword()) :: GenServer.on_start() 13 | def start_link(options) when is_list(options) do 14 | with {:ok, pid} <- GenServer.start_link(__MODULE__, options) do 15 | {:ok, %__MODULE__{pid: pid}} 16 | end 17 | end 18 | 19 | @spec accept(t()) :: {:ok, LRCP.socket()} | {:error, term()} 20 | def accept(%__MODULE__{pid: pid} = _listen_socket) do 21 | GenServer.call(pid, :accept, _timeout = :infinity) 22 | end 23 | 24 | ## Callbacks 25 | 26 | defmodule State do 27 | defstruct [ 28 | :udp_socket, 29 | :supervisor, 30 | accept_queue: :queue.new(), 31 | ready_sockets: :queue.new() 32 | ] 33 | end 34 | 35 | @impl true 36 | def init(options) do 37 | ip = Keyword.fetch!(options, :ip) 38 | port = Keyword.fetch!(options, :port) 39 | 40 | udp_options = [ 41 | :binary, 42 | active: :once, 43 | recbuf: 10_000, 44 | ip: ip 45 | ] 46 | 47 | Logger.metadata(address: "#{:inet.ntoa(ip)}:#{port}") 48 | 49 | with {:ok, udp_socket} <- :gen_udp.open(port, udp_options), 50 | {:ok, supervisor} <- DynamicSupervisor.start_link(max_children: 200) do 51 | Logger.debug("Listening for UDP connections") 52 | {:ok, %State{udp_socket: udp_socket, supervisor: supervisor}} 53 | else 54 | {:error, reason} -> {:stop, reason} 55 | end 56 | end 57 | 58 | @impl true 59 | def handle_call(:accept, from, state) do 60 | case get_and_update_in(state.ready_sockets, &:queue.out/1) do 61 | # There is a socket ready to be handled. 62 | {{:value, %LRCP.Socket{} = socket}, state} -> 63 | Logger.debug("Accepted connection #{inspect(socket)} from the queue") 64 | {:reply, {:ok, socket}, state} 65 | 66 | # No sockets are ready, so we queue this client for when a socket is ready. 67 | {:empty, state} -> 68 | state = update_in(state.accept_queue, &:queue.in(from, &1)) 69 | Logger.debug("Queued accept call") 70 | {:noreply, state} 71 | end 72 | end 73 | 74 | @impl true 75 | def handle_info({:udp, udp_socket, ip, port, packet}, %State{udp_socket: udp_socket} = state) do 76 | :ok = :inet.setopts(udp_socket, active: :once) 77 | Logger.debug("<-- #{inspect(packet)}") 78 | 79 | case LRCP.Protocol.parse_packet(packet) do 80 | {:ok, packet} -> 81 | handle_packet(state, ip, port, packet) 82 | 83 | :error -> 84 | Logger.debug("Invalid packet, ignoring it: #{inspect(packet)}") 85 | {:noreply, state} 86 | end 87 | end 88 | 89 | ## Helpers 90 | 91 | defp handle_packet(state, ip, port, {:connect, session_id}) do 92 | spec = {LRCP.Socket, [%__MODULE__{pid: self()}, state.udp_socket, ip, port, session_id]} 93 | 94 | case DynamicSupervisor.start_child(state.supervisor, spec) do 95 | # We started a new child. 96 | {:ok, socket_pid} -> 97 | socket = %LRCP.Socket{pid: socket_pid} 98 | 99 | case get_and_update_in(state.accept_queue, &:queue.out/1) do 100 | # If there is a pending accept, we can reply to it. 101 | {{:value, from}, state} -> 102 | Logger.debug("Handing over socket #{inspect(socket)} to queued client") 103 | GenServer.reply(from, {:ok, socket}) 104 | {:noreply, state} 105 | 106 | # If there is nothing blocked on accepting, we queue this socket. 107 | {:empty, state} -> 108 | state = update_in(state.ready_sockets, &:queue.in(socket, &1)) 109 | {:noreply, state} 110 | end 111 | 112 | # The connection for this session ID is already running, so we just resend the ack. 113 | {:error, {:already_started, _pid}} -> 114 | :ok = LRCP.Socket.resend_connect_ack(%__MODULE__{pid: self()}, session_id) 115 | {:noreply, state} 116 | 117 | {:error, reason} -> 118 | Logger.error("Failed to start connection: #{inspect(reason)}") 119 | {:noreply, state} 120 | end 121 | end 122 | 123 | defp handle_packet(state, ip, port, {:close, session_id}) do 124 | _ = LRCP.Socket.close(%__MODULE__{pid: self()}, session_id) 125 | send_close(state, ip, port, session_id) 126 | {:noreply, state} 127 | end 128 | 129 | defp handle_packet(state, ip, port, packet) do 130 | case LRCP.Socket.handle_packet(%__MODULE__{pid: self()}, packet) do 131 | :ok -> :ok 132 | :not_found -> send_close(state, ip, port, LRCP.Protocol.session_id(packet)) 133 | end 134 | 135 | {:noreply, state} 136 | end 137 | 138 | defp send_close(state, ip, port, session_id) do 139 | Logger.debug("--> \"/close/#{session_id}/\"") 140 | :ok = :gen_udp.send(state.udp_socket, ip, port, "/close/#{session_id}/") 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /apps/line_reversal/lib/line_reversal/lrcp/protocol.ex: -------------------------------------------------------------------------------- 1 | defmodule LineReversal.LRCP.Protocol do 2 | @type session_id() :: integer() 3 | 4 | @type packet() :: 5 | {:connect, session_id()} 6 | | {:close, session_id()} 7 | | {:data, session_id(), integer(), binary()} 8 | | {:ack, session_id(), integer()} 9 | 10 | @max_int 2_147_483_648 11 | 12 | @spec session_id(packet()) :: session_id() 13 | def session_id({:connect, session_id}), do: session_id 14 | def session_id({:close, session_id}), do: session_id 15 | def session_id({:data, session_id, _position, _data}), do: session_id 16 | def session_id({:ack, session_id, _position}), do: session_id 17 | 18 | @spec parse_packet(binary()) :: {:ok, packet()} | :error 19 | def parse_packet(binary) do 20 | with <> <- binary, 21 | {:ok, parts} <- split(rest, _acc = [], _part = <<>>) do 22 | parse_packet_fields(parts) 23 | else 24 | _other -> :error 25 | end 26 | end 27 | 28 | defp split(<<>>, _acc, _part), do: :error 29 | defp split(<> = _end, acc, part), do: {:ok, Enum.reverse([part | acc])} 30 | defp split(<<"\\/", rest::binary>>, acc, part), do: split(rest, acc, <>) 31 | defp split(<>, acc, part), do: split(rest, [part | acc], <<>>) 32 | defp split(<>, acc, part), do: split(rest, acc, <>) 33 | 34 | defp parse_packet_fields([type, session_id]) when type in ["connect", "close"] do 35 | with {:ok, session_id} <- parse_int(session_id) do 36 | {:ok, {String.to_existing_atom(type), session_id}} 37 | end 38 | end 39 | 40 | defp parse_packet_fields(["data", session_id, position, data]) do 41 | with {:ok, session_id} <- parse_int(session_id), 42 | {:ok, position} <- parse_int(position) do 43 | {:ok, {:data, session_id, position, data}} 44 | end 45 | end 46 | 47 | defp parse_packet_fields(["ack", session_id, position]) do 48 | with {:ok, session_id} <- parse_int(session_id), 49 | {:ok, position} <- parse_int(position) do 50 | {:ok, {:ack, session_id, position}} 51 | end 52 | end 53 | 54 | defp parse_packet_fields(_other) do 55 | :error 56 | end 57 | 58 | defp parse_int(bin) do 59 | case Integer.parse(bin) do 60 | {int, ""} when int < @max_int -> {:ok, int} 61 | _ -> :error 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /apps/line_reversal/lib/line_reversal/lrcp/socket.ex: -------------------------------------------------------------------------------- 1 | defmodule LineReversal.LRCP.Socket do 2 | use GenServer, restart: :temporary 3 | 4 | import Kernel, except: [send: 2] 5 | 6 | alias LineReversal.LRCP 7 | 8 | require Logger 9 | 10 | @type t() :: %__MODULE__{pid: pid()} 11 | 12 | @max_data_length 1_000 - String.length("/data/2147483648/2147483648//") 13 | @idle_timeout 60_000 14 | 15 | if Mix.env() == :test do 16 | @retransmit_interval 100 17 | else 18 | @retransmit_interval 3_000 19 | end 20 | 21 | defstruct [:pid] 22 | 23 | @spec start_link(list()) :: GenServer.on_start() 24 | def start_link([ 25 | %LRCP.ListenSocket{} = listen_socket, 26 | udp_socket, 27 | peer_ip, 28 | peer_port, 29 | session_id 30 | ]) do 31 | name = name(listen_socket, session_id) 32 | GenServer.start_link(__MODULE__, {udp_socket, peer_ip, peer_port, session_id}, name: name) 33 | end 34 | 35 | @spec send(t(), binary()) :: :ok | {:error, term()} 36 | def send(%__MODULE__{} = socket, data) when is_binary(data) do 37 | GenServer.call(socket.pid, {:send, data}) 38 | end 39 | 40 | @spec controlling_process(t(), pid()) :: :ok 41 | def controlling_process(%__MODULE__{} = socket, pid) when is_pid(pid) do 42 | GenServer.call(socket.pid, {:controlling_process, pid}) 43 | catch 44 | :exit, {:noproc, _} -> 45 | Kernel.send(pid, {:lrcp_closed, socket}) 46 | :ok 47 | end 48 | 49 | @spec resend_connect_ack(LRCP.listen_socket(), integer()) :: :ok 50 | def resend_connect_ack(%LRCP.ListenSocket{} = listen_socket, session_id) do 51 | GenServer.cast(name(listen_socket, session_id), :resend_connect_ack) 52 | end 53 | 54 | @spec handle_packet(LRCP.listen_socket(), packet) :: :ok | :not_found 55 | when packet: 56 | {:data, LRCP.Protocol.session_id(), integer(), binary()} 57 | | {:ack, LRCP.Protocol.session_id(), integer()} 58 | def handle_packet(%LRCP.ListenSocket{} = listen_socket, packet) when is_tuple(packet) do 59 | session_id = LRCP.Protocol.session_id(packet) 60 | 61 | if pid = GenServer.whereis(name(listen_socket, session_id)) do 62 | GenServer.cast(pid, {:handle_packet, packet}) 63 | else 64 | :not_found 65 | end 66 | end 67 | 68 | @spec close(LRCP.listen_socket(), integer()) :: :ok 69 | def close(%LRCP.ListenSocket{} = listen_socket, session_id) do 70 | GenServer.cast(name(listen_socket, session_id), :close) 71 | end 72 | 73 | defp name(%LRCP.ListenSocket{} = listen_socket, session_id) when is_integer(session_id) do 74 | {:via, Registry, {LineReversal.Registry, {listen_socket, session_id}}} 75 | end 76 | 77 | ## Callbacks 78 | 79 | defmodule State do 80 | defstruct [ 81 | :udp_socket, 82 | :peer_ip, 83 | :peer_port, 84 | :session_id, 85 | :controlling_process, 86 | :idle_timer_ref, 87 | in_position: 0, 88 | out_position: 0, 89 | acked_out_position: 0, 90 | pending_out_payload: <<>>, 91 | out_message_queue: :queue.new() 92 | ] 93 | end 94 | 95 | @impl true 96 | def init({udp_socket, peer_ip, peer_port, session_id}) do 97 | Logger.metadata(session: session_id) 98 | 99 | idle_timer_ref = Process.send_after(self(), :idle_timeout, @idle_timeout) 100 | 101 | state = %State{ 102 | udp_socket: udp_socket, 103 | peer_ip: peer_ip, 104 | peer_port: peer_port, 105 | session_id: session_id, 106 | idle_timer_ref: idle_timer_ref 107 | } 108 | 109 | udp_send(state, "/ack/#{state.session_id}/0/") 110 | 111 | {:ok, state} 112 | end 113 | 114 | @impl true 115 | def handle_info(message, state) 116 | 117 | def handle_info(:idle_timeout, %State{} = state) do 118 | Logger.info("Closing connection due to inactivity") 119 | {:stop, :normal, state} 120 | end 121 | 122 | def handle_info(:retransmit_pending_data, %State{} = state) do 123 | state = update_in(state.out_position, &(&1 - byte_size(state.pending_out_payload))) 124 | {:noreply, send_data(state, state.pending_out_payload)} 125 | end 126 | 127 | @impl true 128 | def handle_call({:send, data}, _from, %State{} = state) do 129 | state = update_in(state.pending_out_payload, &(&1 <> data)) 130 | state = send_data(state, data) 131 | {:reply, :ok, state} 132 | end 133 | 134 | def handle_call({:controlling_process, pid}, _from, %State{} = state) do 135 | Logger.debug("Controlling process set to #{inspect(pid)}") 136 | state = put_in(state.controlling_process, pid) 137 | 138 | {messages, state} = 139 | get_and_update_in(state.out_message_queue, fn queue -> 140 | {:queue.to_list(queue), :queue.new()} 141 | end) 142 | 143 | Enum.each(messages, &Kernel.send(pid, &1)) 144 | 145 | {:reply, :ok, state} 146 | end 147 | 148 | @impl true 149 | def handle_cast(cast, state) 150 | 151 | def handle_cast(:close, %State{} = state) do 152 | {:stop, :normal, state} 153 | end 154 | 155 | def handle_cast(:resend_connect_ack, %State{} = state) do 156 | udp_send(state, "/ack/#{state.session_id}/#{state.in_position}/") 157 | {:noreply, state} 158 | end 159 | 160 | def handle_cast({:handle_packet, {:data, _session_id, position, data}}, %State{} = state) do 161 | state = reset_idle_timer(state) 162 | 163 | if position == state.in_position do 164 | unescaped_data = unescape_data(data) 165 | state = update_in(state.in_position, &(&1 + byte_size(unescaped_data))) 166 | udp_send(state, "/ack/#{state.session_id}/#{state.in_position}/") 167 | state = send_or_queue_message(state, {:lrcp, %__MODULE__{pid: self()}, unescaped_data}) 168 | {:noreply, state} 169 | else 170 | # If we're not caught up, we resend the ack with the position of where 171 | # we're caught up and keep going. 172 | udp_send(state, "/ack/#{state.session_id}/#{state.in_position}/") 173 | {:noreply, state} 174 | end 175 | end 176 | 177 | def handle_cast({:handle_packet, {:ack, _session_id, length}}, %State{} = state) do 178 | cond do 179 | length <= state.acked_out_position -> 180 | # Do nothing and stop, it's probably a duplicate ack. 181 | Logger.debug("Ignoring ack for #{length} bytes, we've already acked that") 182 | {:noreply, state} 183 | 184 | length > state.out_position -> 185 | # Client is misbehaving, close the session. 186 | Logger.debug( 187 | "Client is misbehaving, closing session (sent ack for position #{length} " <> 188 | "but we've only sent #{state.out_position} bytes)" 189 | ) 190 | 191 | udp_send(state, "/close/#{state.session_id}/") 192 | 193 | state = 194 | send_or_queue_message( 195 | state, 196 | {:lrcp_error, %__MODULE__{pid: self()}, :client_misbehaving} 197 | ) 198 | 199 | {:stop, :normal, state} 200 | 201 | length < state.acked_out_position + byte_size(state.pending_out_payload) -> 202 | transmitted_bytes = length - state.acked_out_position 203 | Logger.debug("Partial ack for #{transmitted_bytes} bytes") 204 | 205 | still_pending_payload = 206 | :binary.part( 207 | state.pending_out_payload, 208 | transmitted_bytes, 209 | byte_size(state.pending_out_payload) - transmitted_bytes 210 | ) 211 | 212 | udp_send( 213 | state, 214 | "/data/#{state.session_id}/#{state.acked_out_position + transmitted_bytes}/" <> 215 | escape_data(still_pending_payload) <> "/" 216 | ) 217 | 218 | state = put_in(state.acked_out_position, length) 219 | state = put_in(state.pending_out_payload, still_pending_payload) 220 | {:noreply, state} 221 | 222 | length == state.out_position -> 223 | Logger.debug("Everything we've sent has been acked") 224 | state = put_in(state.acked_out_position, length) 225 | state = put_in(state.pending_out_payload, <<>>) 226 | {:noreply, state} 227 | 228 | true -> 229 | raise """ 230 | Should never reach this. 231 | 232 | state: #{inspect(state)} 233 | length: #{length} 234 | """ 235 | end 236 | end 237 | 238 | ## Helpers 239 | 240 | defp send_data(%State{} = state, <<>>) do 241 | Process.send_after(self(), :retransmit_pending_data, @retransmit_interval) 242 | state 243 | end 244 | 245 | defp send_data(%State{} = state, data) do 246 | {chunk, rest} = 247 | case data do 248 | <> -> {chunk, rest} 249 | chunk -> {chunk, ""} 250 | end 251 | 252 | udp_send(state, "/data/#{state.session_id}/#{state.out_position}/#{escape_data(chunk)}/") 253 | state = update_in(state.out_position, &(&1 + byte_size(chunk))) 254 | 255 | send_data(state, rest) 256 | end 257 | 258 | defp send_or_queue_message(%State{} = state, message) do 259 | if state.controlling_process do 260 | Logger.debug("Sending message to client: #{inspect(message)}") 261 | Kernel.send(state.controlling_process, message) 262 | state 263 | else 264 | Logger.debug("Queueing message: #{inspect(message)}") 265 | update_in(state.out_message_queue, &:queue.in(message, &1)) 266 | end 267 | end 268 | 269 | defp escape_data(data) do 270 | data 271 | |> String.replace("\\", "\\\\") 272 | |> String.replace("/", "\\/") 273 | end 274 | 275 | defp unescape_data(data) do 276 | data 277 | |> String.replace("\\/", "/") 278 | |> String.replace("\\\\", "\\") 279 | end 280 | 281 | defp udp_send(%State{} = state, data) do 282 | Logger.debug("--> #{inspect(data)}") 283 | :ok = :gen_udp.send(state.udp_socket, state.peer_ip, state.peer_port, data) 284 | end 285 | 286 | defp reset_idle_timer(%State{} = state) do 287 | Process.cancel_timer(state.idle_timer_ref) 288 | idle_timer_ref = Process.send_after(self(), :idle_timeout, @idle_timeout) 289 | put_in(state.idle_timer_ref, idle_timer_ref) 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /apps/line_reversal/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule LineReversal.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :line_reversal, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.14", 13 | start_permanent: Mix.env() == :prod, 14 | deps: deps() 15 | ] 16 | end 17 | 18 | # Run "mix help compile.app" to learn about applications. 19 | def application do 20 | [ 21 | extra_applications: [:logger], 22 | mod: {LineReversal.Application, []} 23 | ] 24 | end 25 | 26 | # Run "mix help deps" to learn about dependencies. 27 | defp deps do 28 | [ 29 | # {:dep_from_hexpm, "~> 0.3.0"}, 30 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}, 31 | # {:sibling_app_in_umbrella, in_umbrella: true} 32 | ] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /apps/line_reversal/mix.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatyouhide/protohackers_in_elixir/f73015610479467c0d60fcaeda6da8dbcc4d6538/apps/line_reversal/mix.lock -------------------------------------------------------------------------------- /apps/line_reversal/rel/env.bat.eex: -------------------------------------------------------------------------------- 1 | @echo off 2 | rem Set the release to load code on demand (interactive) instead of preloading (embedded). 3 | rem set RELEASE_MODE=interactive 4 | 5 | rem Set the release to work across nodes. 6 | rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none". 7 | rem set RELEASE_DISTRIBUTION=name 8 | rem set RELEASE_NODE=<%= @release.name %> 9 | -------------------------------------------------------------------------------- /apps/line_reversal/rel/env.sh.eex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ip=$(grep fly-local-6pn /etc/hosts | cut -f 1) 4 | export RELEASE_DISTRIBUTION=name 5 | export RELEASE_NODE=$FLY_APP_NAME@"$ip" 6 | export ELIXIR_ERL_OPTIONS="-proto_dist inet6_tcp" 7 | -------------------------------------------------------------------------------- /apps/line_reversal/rel/remote.vm.args.eex: -------------------------------------------------------------------------------- 1 | ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html 2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here 3 | 4 | ## Increase number of concurrent ports/sockets 5 | ##+Q 65536 6 | 7 | ## Tweak GC to run more often 8 | ##-env ERL_FULLSWEEP_AFTER 10 9 | -------------------------------------------------------------------------------- /apps/line_reversal/rel/vm.args.eex: -------------------------------------------------------------------------------- 1 | ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html 2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here 3 | 4 | ## Increase number of concurrent ports/sockets 5 | ##+Q 65536 6 | 7 | ## Tweak GC to run more often 8 | ##-env ERL_FULLSWEEP_AFTER 10 9 | -------------------------------------------------------------------------------- /apps/line_reversal/test/line_reversal/lrcp/protocol_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LineReversal.LRCP.ProtocolTest do 2 | use ExUnit.Case, async: true 3 | 4 | import LineReversal.LRCP.Protocol 5 | 6 | @max_int 2_147_483_648 7 | 8 | describe "parse_packet/1" do 9 | test "invalid packets" do 10 | assert parse_packet("") == :error 11 | assert parse_packet("/") == :error 12 | assert parse_packet("//") == :error 13 | assert parse_packet("/connect") == :error 14 | assert parse_packet("/connect/1") == :error 15 | assert parse_packet("connect/1/") == :error 16 | end 17 | 18 | test "returns an error for integers that are too large" do 19 | assert parse_packet("/connect/#{@max_int}/") == :error 20 | assert parse_packet("/ack/#{@max_int}/1/") == :error 21 | assert parse_packet("/ack/1/#{@max_int}/") == :error 22 | end 23 | 24 | test "connect packet" do 25 | assert parse_packet("/connect/231/") == {:ok, {:connect, 231}} 26 | end 27 | 28 | test "close packet" do 29 | assert parse_packet("/close/231/") == {:ok, {:close, 231}} 30 | end 31 | 32 | test "ack packet" do 33 | assert parse_packet("/ack/123/456/") == {:ok, {:ack, 123, 456}} 34 | end 35 | 36 | test "data packet" do 37 | assert parse_packet("/data/123/456/hello\\/world\\\\!\n/") == 38 | {:ok, {:data, 123, 456, "hello\\/world\\\\!\n"}} 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /apps/line_reversal/test/line_reversal/udp_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LineReversal.UDPServerTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "connecting and closing multiple clients" do 5 | {client1, session_id1} = open_udp() 6 | {client2, session_id2} = open_udp() 7 | 8 | udp_send(client1, "/connect/#{session_id1}/") 9 | udp_send(client2, "/connect/#{session_id2}/") 10 | 11 | assert udp_recv(client1) == "/ack/#{session_id1}/0/" 12 | assert udp_recv(client2) == "/ack/#{session_id2}/0/" 13 | 14 | # Closing 15 | 16 | udp_send(client1, "/close/#{session_id1}/") 17 | assert udp_recv(client1) == "/close/#{session_id1}/" 18 | 19 | udp_send(client2, "/close/#{session_id2}/") 20 | assert udp_recv(client2) == "/close/#{session_id2}/" 21 | end 22 | 23 | test "server ignores invalid messages" do 24 | {client, session_id} = open_udp() 25 | 26 | udp_send(client, "invalid") 27 | udp_send(client, "/connect/1") 28 | udp_send(client, "connect/1/") 29 | udp_send(client, "/ack/") 30 | udp_send(client, "//") 31 | 32 | # We can still connect after invalid messages 33 | udp_send(client, "/connect/#{session_id}/") 34 | assert udp_recv(client) == "/ack/#{session_id}/0/" 35 | end 36 | 37 | test "servers sends CLOSE if sending a packet to a dead client" do 38 | {client, session_id} = open_udp() 39 | 40 | # Connect 41 | udp_send(client, "/connect/#{session_id}/") 42 | assert udp_recv(client) == "/ack/#{session_id}/0/" 43 | 44 | # Close 45 | udp_send(client, "/close/#{session_id}/") 46 | assert udp_recv(client) == "/close/#{session_id}/" 47 | 48 | # Send a packet to a dead client and get another close message 49 | udp_send(client, "/data/#{session_id}/1/hello/") 50 | assert udp_recv(client) == "/close/#{session_id}/" 51 | end 52 | 53 | test "server receives data and sends the appropriate acks" do 54 | {client, session_id} = open_udp() 55 | 56 | # Connect 57 | udp_send(client, "/connect/#{session_id}/") 58 | assert udp_recv(client) == "/ack/#{session_id}/0/" 59 | 60 | # If we send a packet with invalid position (greater than 0 initially), we receive 61 | # an ack for 0. 62 | udp_send(client, "/data/#{session_id}/1/hello/") 63 | assert udp_recv(client) == "/ack/#{session_id}/0/" 64 | 65 | # Now we send real data at the right position, we should get the right ack. 66 | udp_send(client, "/data/#{session_id}/0/hello/") 67 | assert udp_recv(client) == "/ack/#{session_id}/5/" 68 | 69 | # If we send more data, we should get the right ack again. 70 | udp_send(client, "/data/#{session_id}/5/\\//") 71 | assert udp_recv(client) == "/ack/#{session_id}/6/" 72 | 73 | # If we send data with the wrong position, we get the ack for the last position. 74 | udp_send(client, "/data/#{session_id}/3/wrongpos/") 75 | assert udp_recv(client) == "/ack/#{session_id}/6/" 76 | end 77 | 78 | test "example session from the problem" do 79 | assert {:ok, client} = :gen_udp.open(0, [:binary, active: false]) 80 | 81 | # Connect 82 | udp_send(client, "/connect/12345/") 83 | assert udp_recv(client) == "/ack/12345/0/" 84 | 85 | udp_send(client, "/data/12345/0/hello\n/") 86 | assert udp_recv(client) == "/ack/12345/6/" 87 | 88 | assert udp_recv(client) == "/data/12345/0/olleh\n/" 89 | udp_send(client, "/ack/12345/6/") 90 | 91 | udp_send(client, "/data/12345/6/Hello, world!\n/") 92 | assert udp_recv(client) == "/ack/12345/20/" 93 | 94 | assert udp_recv(client) == "/data/12345/6/!dlrow ,olleH\n/" 95 | 96 | udp_send(client, "/ack/12345/20/") 97 | 98 | udp_send(client, "/close/12345/") 99 | assert udp_recv(client) == "/close/12345/" 100 | end 101 | 102 | test "closing the same session multiple times" do 103 | {client, session_id} = open_udp() 104 | udp_send(client, "/connect/#{session_id}/") 105 | assert udp_recv(client) == "/ack/#{session_id}/0/" 106 | 107 | udp_send(client, "/close/#{session_id}/") 108 | assert udp_recv(client) == "/close/#{session_id}/" 109 | udp_send(client, "/close/#{session_id}/") 110 | assert udp_recv(client) == "/close/#{session_id}/" 111 | end 112 | 113 | test "data sent in broken packets" do 114 | {client, session_id} = open_udp() 115 | udp_send(client, "/connect/#{session_id}/") 116 | assert udp_recv(client) == "/ack/#{session_id}/0/" 117 | 118 | udp_send(client, "/data/#{session_id}/0/hello /") 119 | assert udp_recv(client) == "/ack/#{session_id}/6/" 120 | 121 | udp_send(client, "/data/#{session_id}/6/world!/") 122 | assert udp_recv(client) == "/ack/#{session_id}/12/" 123 | 124 | udp_send(client, "/data/#{session_id}/12/\\/\n/") 125 | assert udp_recv(client) == "/ack/#{session_id}/14/" 126 | 127 | assert udp_recv(client) == "/data/#{session_id}/0/\\/#{String.reverse("hello world!")}\n/" 128 | end 129 | 130 | test "multiple sessions in parallel" do 131 | tasks = 132 | for _ <- 1..20 do 133 | session_id = System.unique_integer([:positive]) 134 | 135 | Task.async(fn -> 136 | assert {:ok, client} = :gen_udp.open(0, [:binary, active: false]) 137 | udp_send(client, "/connect/#{session_id}/") 138 | assert udp_recv(client) == "/ack/#{session_id}/0/" 139 | 140 | udp_send(client, "/data/#{session_id}/0/hello\n/") 141 | assert udp_recv(client) == "/ack/#{session_id}/6/" 142 | 143 | assert udp_recv(client) == "/data/#{session_id}/0/olleh\n/" 144 | udp_send(client, "/ack/#{session_id}/6/") 145 | 146 | udp_send(client, "/close/#{session_id}/") 147 | assert udp_recv(client) == "/close/#{session_id}/" 148 | 149 | :done 150 | end) 151 | end 152 | 153 | assert Enum.all?(Task.yield_many(tasks, 5000), &match?({_task, {:ok, :done}}, &1)) 154 | end 155 | 156 | test "multiple lines in a single UDP packet" do 157 | {client, session_id} = open_udp() 158 | udp_send(client, "/connect/#{session_id}/") 159 | assert udp_recv(client) == "/ack/#{session_id}/0/" 160 | 161 | udp_send(client, "/data/#{session_id}/0/abcd\n1234\nfoo/") 162 | assert udp_recv(client) == "/ack/#{session_id}/13/" 163 | 164 | assert udp_recv(client) == "/data/#{session_id}/0/dcba\n/" 165 | assert udp_recv(client) == "/data/#{session_id}/5/4321\n/" 166 | end 167 | 168 | test "sending and receiving big data" do 169 | {client, session_id} = open_udp() 170 | udp_send(client, "/connect/#{session_id}/") 171 | assert udp_recv(client) == "/ack/#{session_id}/0/" 172 | 173 | data_to_send = :binary.copy("a", 700) 174 | 175 | udp_send(client, "/data/#{session_id}/0/#{data_to_send}/") 176 | assert udp_recv(client) == "/ack/#{session_id}/700/" 177 | udp_send(client, "/data/#{session_id}/700/#{data_to_send}\n/") 178 | assert udp_recv(client) == "/ack/#{session_id}/1401/" 179 | 180 | assert udp_recv(client) == "/data/#{session_id}/0/#{:binary.copy("a", 971)}/" 181 | assert udp_recv(client) == "/data/#{session_id}/971/#{:binary.copy("a", 1400 - 971)}\n/" 182 | end 183 | 184 | @tag :capture_log 185 | test "server sends close message if clients misbehaves with acks and positions" do 186 | {client, session_id} = open_udp() 187 | udp_send(client, "/connect/#{session_id}/") 188 | assert udp_recv(client) == "/ack/#{session_id}/0/" 189 | 190 | len = String.length("either/or\n") 191 | 192 | udp_send(client, "/data/#{session_id}/0/either\\/or\n/") 193 | assert udp_recv(client) == "/ack/#{session_id}/#{len}/" 194 | 195 | assert udp_recv(client) == "/data/#{session_id}/0/ro\\/rehtie\n/" 196 | 197 | # Ack only two bytes, which means that the server should resend us the rest of the data. 198 | udp_send(client, "/ack/#{session_id}/2/") 199 | assert udp_recv(client) == "/data/#{session_id}/2/\\/rehtie\n/" 200 | 201 | # Ack another two bytes and let the server resend the rest. 202 | udp_send(client, "/ack/#{session_id}/3/") 203 | assert udp_recv(client) == "/data/#{session_id}/3/rehtie\n/" 204 | 205 | # If we ack something we already acked again, nothing happens. 206 | udp_send(client, "/ack/#{session_id}/1/") 207 | 208 | # If we hack further than what the server sent us, it's a protocol error. 209 | udp_send(client, "/ack/#{session_id}/1000/") 210 | assert udp_recv(client) == "/close/#{session_id}/" 211 | 212 | Process.sleep(100) 213 | end 214 | 215 | test "server retrasmits data if it doesn't receive acks" do 216 | {client, session_id} = open_udp() 217 | 218 | udp_send(client, "/connect/#{session_id}/") 219 | assert udp_recv(client) == "/ack/#{session_id}/0/" 220 | 221 | udp_send(client, "/data/#{session_id}/0/hello\n/") 222 | assert udp_recv(client) == "/ack/#{session_id}/6/" 223 | 224 | assert udp_recv(client) == "/data/#{session_id}/0/olleh\n/" 225 | assert udp_recv(client) == "/data/#{session_id}/0/olleh\n/" 226 | assert udp_recv(client) == "/data/#{session_id}/0/olleh\n/" 227 | 228 | udp_send(client, "/ack/#{session_id}/3/") 229 | assert udp_recv(client) == "/data/#{session_id}/3/eh\n/" 230 | assert udp_recv(client) == "/data/#{session_id}/3/eh\n/" 231 | end 232 | 233 | ## Helper 234 | 235 | defp open_udp do 236 | session_id = System.unique_integer([:positive]) 237 | assert {:ok, socket} = :gen_udp.open(0, [:binary, active: false]) 238 | {socket, session_id} 239 | end 240 | 241 | defp udp_recv(client) do 242 | assert {:ok, {_ip, _port, data}} = :gen_udp.recv(client, 0, 2_000) 243 | data 244 | end 245 | 246 | defp udp_send(client, data) do 247 | assert :ok = :gen_udp.send(client, {127, 0, 0, 1}, 5008, data) 248 | end 249 | end 250 | -------------------------------------------------------------------------------- /apps/line_reversal/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/lib/protohackers/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | children = [ 11 | {Protohackers.EchoServer, port: 5001}, 12 | {Protohackers.PrimeServer, port: 5002}, 13 | {Protohackers.PricesServer, port: 5003}, 14 | {Protohackers.BudgetChatServer, port: 5004}, 15 | {Protohackers.UDPServer, port: 5005}, 16 | {Protohackers.MITM.Supervisor, port: 5006}, 17 | ] 18 | 19 | # See https://hexdocs.pm/elixir/Supervisor.html 20 | # for other strategies and supported options 21 | opts = [strategy: :one_for_one, name: Protohackers.Supervisor] 22 | Supervisor.start_link(children, opts) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/lib/protohackers/budget_chat_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.BudgetChatServer do 2 | use GenServer 3 | 4 | require Logger 5 | 6 | @spec start_link(keyword()) :: GenServer.on_start() 7 | def start_link(opts) do 8 | GenServer.start_link(__MODULE__, opts) 9 | end 10 | 11 | defstruct [:listen_socket, :supervisor, :ets] 12 | 13 | @impl true 14 | def init(opts) do 15 | port = Keyword.fetch!(opts, :port) 16 | {:ok, supervisor} = Task.Supervisor.start_link(max_children: 100) 17 | 18 | ets = :ets.new(__MODULE__, [:public]) 19 | 20 | listen_options = [ 21 | ifaddr: {0, 0, 0, 0}, 22 | mode: :binary, 23 | active: false, 24 | reuseaddr: true, 25 | exit_on_close: false, 26 | packet: :line, 27 | buffer: 1024 * 100 28 | ] 29 | 30 | case :gen_tcp.listen(port, listen_options) do 31 | {:ok, listen_socket} -> 32 | Logger.info("Started server on port #{port}") 33 | state = %__MODULE__{listen_socket: listen_socket, supervisor: supervisor, ets: ets} 34 | {:ok, state, {:continue, :accept}} 35 | 36 | {:error, reason} -> 37 | {:stop, reason} 38 | end 39 | end 40 | 41 | @impl true 42 | def handle_continue(:accept, %__MODULE__{} = state) do 43 | case :gen_tcp.accept(state.listen_socket) do 44 | {:ok, socket} -> 45 | Task.Supervisor.start_child(state.supervisor, fn -> 46 | handle_connection(socket, state.ets) 47 | end) 48 | 49 | {:noreply, state, {:continue, :accept}} 50 | 51 | {:error, reason} -> 52 | {:stop, reason} 53 | end 54 | end 55 | 56 | ## Helpers 57 | 58 | defp handle_connection(socket, ets) do 59 | :ok = :gen_tcp.send(socket, "What's your username?\n") 60 | 61 | case :gen_tcp.recv(socket, 0, 300_000) do 62 | {:ok, line} -> 63 | username = String.trim(line) 64 | 65 | if username =~ ~r/^[[:alnum:]]+$/ do 66 | Logger.debug("Username #{username} connected") 67 | all_users = :ets.match(ets, :"$1") 68 | usernames = Enum.map_join(all_users, ", ", fn [{_socket, username}] -> username end) 69 | :ets.insert(ets, {socket, username}) 70 | 71 | Enum.each(all_users, fn [{socket, _username}] -> 72 | :gen_tcp.send(socket, "* #{username} has entered the chat\n") 73 | end) 74 | 75 | :ok = :gen_tcp.send(socket, "* The room contains: #{usernames}\n") 76 | handle_chat_session(socket, ets, username) 77 | else 78 | :ok = :gen_tcp.send(socket, "Invalid username\n") 79 | :gen_tcp.close(socket) 80 | end 81 | 82 | {:error, _reason} -> 83 | :gen_tcp.close(socket) 84 | :ok 85 | end 86 | end 87 | 88 | def handle_chat_session(socket, ets, username) do 89 | case :gen_tcp.recv(socket, 0, 300_000) do 90 | {:ok, message} -> 91 | message = String.trim(message) 92 | 93 | if message != "" do 94 | all_sockets = :ets.match(ets, {:"$1", :_}) 95 | 96 | for [other_socket] <- all_sockets, other_socket != socket do 97 | :gen_tcp.send(other_socket, "[#{username}] #{message}\n") 98 | end 99 | end 100 | 101 | handle_chat_session(socket, ets, username) 102 | 103 | {:error, _reason} -> 104 | all_sockets = :ets.match(ets, {:"$1", :_}) 105 | 106 | for [other_socket] <- all_sockets, other_socket != socket do 107 | :gen_tcp.send(other_socket, "* #{username} left\n") 108 | end 109 | 110 | _ = :gen_tcp.close(socket) 111 | :ets.delete(ets, socket) 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/lib/protohackers/echo_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.EchoServer do 2 | use GenServer 3 | 4 | require Logger 5 | 6 | @spec start_link(keyword()) :: GenServer.on_start() 7 | def start_link(opts) do 8 | GenServer.start_link(__MODULE__, opts) 9 | end 10 | 11 | defstruct [:listen_socket, :supervisor] 12 | 13 | @impl true 14 | def init(opts) do 15 | port = Keyword.fetch!(opts, :port) 16 | {:ok, supervisor} = Task.Supervisor.start_link(max_children: 100) 17 | 18 | listen_options = [ 19 | ifaddr: {0, 0, 0, 0}, 20 | mode: :binary, 21 | active: false, 22 | reuseaddr: true, 23 | exit_on_close: false 24 | ] 25 | 26 | case :gen_tcp.listen(port, listen_options) do 27 | {:ok, listen_socket} -> 28 | Logger.info("Staretd server on port #{port}") 29 | state = %__MODULE__{listen_socket: listen_socket, supervisor: supervisor} 30 | {:ok, state, {:continue, :accept}} 31 | 32 | {:error, reason} -> 33 | {:stop, reason} 34 | end 35 | end 36 | 37 | @impl true 38 | def handle_continue(:accept, %__MODULE__{} = state) do 39 | case :gen_tcp.accept(state.listen_socket) do 40 | {:ok, socket} -> 41 | Task.Supervisor.start_child(state.supervisor, fn -> handle_connection(socket) end) 42 | {:noreply, state, {:continue, :accept}} 43 | 44 | {:error, reason} -> 45 | {:stop, reason} 46 | end 47 | end 48 | 49 | ## Helpers 50 | 51 | defp handle_connection(socket) do 52 | case recv_until_closed(socket, _buffer = "", _buffered_size = 0) do 53 | {:ok, data} -> :gen_tcp.send(socket, data) 54 | {:error, reason} -> Logger.error("Failed to receive data: #{inspect(reason)}") 55 | end 56 | 57 | :gen_tcp.close(socket) 58 | end 59 | 60 | @limit _100_kb = 1024 * 100 61 | 62 | defp recv_until_closed(socket, buffer, buffered_size) do 63 | case :gen_tcp.recv(socket, 0, 10_000) do 64 | {:ok, data} when buffered_size + byte_size(data) > @limit -> {:error, :buffer_overflow} 65 | {:ok, data} -> recv_until_closed(socket, [buffer, data], buffered_size + byte_size(data)) 66 | {:error, :closed} -> {:ok, buffer} 67 | {:error, reason} -> {:error, reason} 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/lib/protohackers/mitm/acceptor.ex: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.MITM.Acceptor do 2 | use Task, restart: :transient 3 | 4 | require Logger 5 | 6 | @spec start_link(keyword()) :: GenServer.on_start() 7 | def start_link(opts) do 8 | Task.start_link(__MODULE__, :run, [Keyword.fetch!(opts, :port)]) 9 | end 10 | 11 | @spec run(:inet.port_number()) :: no_return() 12 | def run(port) do 13 | case :gen_tcp.listen(port, [ 14 | :binary, 15 | ifaddr: {0, 0, 0, 0}, 16 | active: :once, 17 | packet: :line, 18 | reuseaddr: true 19 | ]) do 20 | {:ok, listen_socket} -> 21 | Logger.info("MITM server listening on port #{port}") 22 | accept_loop(listen_socket) 23 | 24 | {:error, reason} -> 25 | raise "failed to listen on port #{port}: #{inspect(reason)}" 26 | end 27 | end 28 | 29 | defp accept_loop(listen_socket) do 30 | case :gen_tcp.accept(listen_socket) do 31 | {:ok, socket} -> 32 | {:ok, _} = Protohackers.MITM.ConnectionSupervisor.start_child(socket) 33 | accept_loop(listen_socket) 34 | 35 | {:error, reason} -> 36 | raise "failed to accept connection: #{inspect(reason)}" 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/lib/protohackers/mitm/boguscoin.ex: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.MITM.Boguscoin do 2 | @tonys_address "7YWHMfk9JZe0LM0g1ZauHuiSxhI" 3 | 4 | @spec rewrite_addresses(binary()) :: binary() 5 | def rewrite_addresses(string) when is_binary(string) do 6 | regex = ~r/(^|\s)\K(7[[:alnum:]]{25,34})(?= [^[:alnum:]]|\s|$)/ 7 | Regex.replace(regex, string, @tonys_address) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/lib/protohackers/mitm/connection.ex: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.MITM.Connection do 2 | use GenServer 3 | 4 | require Logger 5 | 6 | @spec start_link(:gen_tcp.socket()) :: GenServer.on_start() 7 | def start_link(incoming_socket) do 8 | GenServer.start_link(__MODULE__, incoming_socket) 9 | end 10 | 11 | defstruct [:incoming_socket, :outgoing_socket] 12 | 13 | @impl true 14 | def init(incoming_socket) do 15 | case :gen_tcp.connect(~c"chat.protohackers.com", 16963, [:binary, active: :once]) do 16 | {:ok, outgoing_socket} -> 17 | Logger.debug("Started connection handler") 18 | {:ok, %__MODULE__{incoming_socket: incoming_socket, outgoing_socket: outgoing_socket}} 19 | 20 | {:error, reason} -> 21 | Logger.error("Failed to connect to the upstream server: #{inspect(reason)}") 22 | {:stop, reason} 23 | end 24 | end 25 | 26 | @impl true 27 | def handle_info(message, state) 28 | 29 | def handle_info( 30 | {:tcp, incoming_socket, data}, 31 | %__MODULE__{incoming_socket: incoming_socket} = state 32 | ) do 33 | :ok = :inet.setopts(incoming_socket, active: :once) 34 | Logger.debug("Received data: #{inspect(data)}") 35 | data = Protohackers.MITM.Boguscoin.rewrite_addresses(data) 36 | :gen_tcp.send(state.outgoing_socket, data) 37 | {:noreply, state} 38 | end 39 | 40 | def handle_info( 41 | {:tcp, outgoing_socket, data}, 42 | %__MODULE__{outgoing_socket: outgoing_socket} = state 43 | ) do 44 | :ok = :inet.setopts(outgoing_socket, active: :once) 45 | Logger.debug("Received data: #{inspect(data)}") 46 | data = Protohackers.MITM.Boguscoin.rewrite_addresses(data) 47 | :gen_tcp.send(state.incoming_socket, data) 48 | {:noreply, state} 49 | end 50 | 51 | def handle_info({:tcp_error, socket, reason}, %__MODULE__{} = state) 52 | when socket in [state.incoming_socket, state.outgoing_socket] do 53 | Logger.error("Received TCP error: #{inspect(reason)}") 54 | :gen_tcp.close(state.incoming_socket) 55 | :gen_tcp.close(state.outgoing_socket) 56 | {:stop, :normal, state} 57 | end 58 | 59 | def handle_info({:tcp_closed, socket}, %__MODULE__{} = state) 60 | when socket in [state.incoming_socket, state.outgoing_socket] do 61 | Logger.debug("TCP connection closed") 62 | :gen_tcp.close(state.incoming_socket) 63 | :gen_tcp.close(state.outgoing_socket) 64 | {:stop, :normal, state} 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/lib/protohackers/mitm/connection_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.MITM.ConnectionSupervisor do 2 | use DynamicSupervisor 3 | 4 | @spec start_link(keyword()) :: Supervisor.on_start() 5 | def start_link([] = _opts) do 6 | DynamicSupervisor.start_link(__MODULE__, :no_args, name: __MODULE__) 7 | end 8 | 9 | @spec start_child(:gen_tcp.socket()) :: DynamicSupervisor.on_start_child() 10 | def start_child(socket) do 11 | child_spec = {Protohackers.MITM.Connection, socket} 12 | 13 | with {:ok, conn} <- DynamicSupervisor.start_child(__MODULE__, child_spec), 14 | :ok <- :gen_tcp.controlling_process(socket, conn) do 15 | {:ok, conn} 16 | end 17 | end 18 | 19 | @impl true 20 | def init(:no_args) do 21 | DynamicSupervisor.init(strategy: :one_for_one, max_children: 50) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/lib/protohackers/mitm/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.MITM.Supervisor do 2 | use Supervisor 3 | 4 | @spec start_link(keyword()) :: Supervisor.on_start() 5 | def start_link(opts) do 6 | Supervisor.start_link(__MODULE__, opts) 7 | end 8 | 9 | @impl true 10 | def init(opts) do 11 | children = [ 12 | {Protohackers.MITM.ConnectionSupervisor, []}, 13 | {Protohackers.MITM.Acceptor, opts} 14 | ] 15 | 16 | Supervisor.init(children, strategy: :rest_for_one) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/lib/protohackers/prices_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.PricesServer do 2 | use GenServer 3 | 4 | alias Protohackers.PricesServer.DB 5 | 6 | require Logger 7 | 8 | @spec start_link(keyword()) :: GenServer.on_start() 9 | def start_link(opts) do 10 | GenServer.start_link(__MODULE__, opts) 11 | end 12 | 13 | defstruct [:listen_socket, :supervisor] 14 | 15 | @impl true 16 | def init(opts) do 17 | port = Keyword.fetch!(opts, :port) 18 | {:ok, supervisor} = Task.Supervisor.start_link(max_children: 100) 19 | 20 | listen_options = [ 21 | ifaddr: {0, 0, 0, 0}, 22 | mode: :binary, 23 | active: false, 24 | reuseaddr: true, 25 | exit_on_close: false, 26 | backlog: 100 27 | ] 28 | 29 | case :gen_tcp.listen(port, listen_options) do 30 | {:ok, listen_socket} -> 31 | Logger.info("Started server on port #{port}") 32 | state = %__MODULE__{listen_socket: listen_socket, supervisor: supervisor} 33 | {:ok, state, {:continue, :accept}} 34 | 35 | {:error, reason} -> 36 | {:stop, reason} 37 | end 38 | end 39 | 40 | @impl true 41 | def handle_continue(:accept, %__MODULE__{} = state) do 42 | case :gen_tcp.accept(state.listen_socket) do 43 | {:ok, socket} -> 44 | Task.Supervisor.start_child(state.supervisor, fn -> handle_connection(socket) end) 45 | {:noreply, state, {:continue, :accept}} 46 | 47 | {:error, reason} -> 48 | {:stop, reason} 49 | end 50 | end 51 | 52 | ## Helpers 53 | 54 | defp handle_connection(socket) do 55 | case handle_requests(socket, DB.new()) do 56 | :ok -> :ok 57 | {:error, reason} -> Logger.error("Failed to receive data: #{inspect(reason)}") 58 | end 59 | 60 | :gen_tcp.close(socket) 61 | end 62 | 63 | defp handle_requests(socket, db) do 64 | case :gen_tcp.recv(socket, 9, 10_000) do 65 | {:ok, data} -> 66 | case handle_request(data, db) do 67 | {nil, db} -> 68 | handle_requests(socket, db) 69 | 70 | {response, db} -> 71 | :gen_tcp.send(socket, response) 72 | handle_requests(socket, db) 73 | 74 | :error -> 75 | {:error, :invalid_request} 76 | end 77 | 78 | {:error, :timeout} -> 79 | handle_requests(socket, db) 80 | 81 | {:error, :closed} -> 82 | :ok 83 | 84 | {:error, reason} -> 85 | {:error, reason} 86 | end 87 | end 88 | 89 | defp handle_request(<>, db) do 90 | {nil, DB.add(db, timestamp, price)} 91 | end 92 | 93 | defp handle_request(<>, db) do 94 | avg = DB.query(db, mintime, maxtime) 95 | {<>, db} 96 | end 97 | 98 | defp handle_request(_other, _db) do 99 | :error 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/lib/protohackers/prices_server/db.ex: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.PricesServer.DB do 2 | @type timestamp() :: integer() 3 | @type price() :: integer() 4 | @type t() :: [{timestamp(), price()}] 5 | 6 | @spec new() :: t() 7 | def new do 8 | [] 9 | end 10 | 11 | @spec add(t(), timestamp(), price()) :: t() 12 | def add(db, timestamp, price) 13 | when is_list(db) and is_integer(timestamp) and is_integer(price) do 14 | [{timestamp, price} | db] 15 | end 16 | 17 | @spec query(t(), timestamp(), timestamp()) :: price() 18 | def query(db, from, to) when is_list(db) and is_integer(from) and is_integer(to) do 19 | db 20 | |> Stream.filter(fn {timestamp, _price} -> timestamp >= from and timestamp <= to end) 21 | |> Stream.map(fn {_timestamp, price} -> price end) 22 | |> Enum.reduce({0, 0}, fn price, {sum, count} -> {sum + price, count + 1} end) 23 | |> then(fn 24 | {_sum, 0} -> 0 25 | {sum, count} -> div(sum, count) 26 | end) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/lib/protohackers/prime_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.PrimeServer do 2 | use GenServer 3 | 4 | require Logger 5 | 6 | @spec start_link(keyword()) :: GenServer.on_start() 7 | def start_link(opts) do 8 | GenServer.start_link(__MODULE__, opts) 9 | end 10 | 11 | defstruct [:listen_socket, :supervisor] 12 | 13 | @impl true 14 | def init(opts) do 15 | port = Keyword.fetch!(opts, :port) 16 | {:ok, supervisor} = Task.Supervisor.start_link(max_children: 100) 17 | 18 | listen_options = [ 19 | ifaddr: {0, 0, 0, 0}, 20 | mode: :binary, 21 | active: false, 22 | reuseaddr: true, 23 | exit_on_close: false, 24 | packet: :line, 25 | buffer: 1024 * 100 26 | ] 27 | 28 | case :gen_tcp.listen(port, listen_options) do 29 | {:ok, listen_socket} -> 30 | Logger.info("Started server on port #{port}") 31 | state = %__MODULE__{listen_socket: listen_socket, supervisor: supervisor} 32 | {:ok, state, {:continue, :accept}} 33 | 34 | {:error, reason} -> 35 | {:stop, reason} 36 | end 37 | end 38 | 39 | @impl true 40 | def handle_continue(:accept, %__MODULE__{} = state) do 41 | case :gen_tcp.accept(state.listen_socket) do 42 | {:ok, socket} -> 43 | Task.Supervisor.start_child(state.supervisor, fn -> handle_connection(socket) end) 44 | {:noreply, state, {:continue, :accept}} 45 | 46 | {:error, reason} -> 47 | {:stop, reason} 48 | end 49 | end 50 | 51 | ## Helpers 52 | 53 | defp handle_connection(socket) do 54 | case echo_lines_until_closed(socket) do 55 | :ok -> :ok 56 | {:error, reason} -> Logger.error("Failed to receive data: #{inspect(reason)}") 57 | end 58 | 59 | :gen_tcp.close(socket) 60 | end 61 | 62 | defp echo_lines_until_closed(socket) do 63 | case :gen_tcp.recv(socket, 0, 10_000) do 64 | {:ok, data} -> 65 | case Jason.decode(data) do 66 | {:ok, %{"method" => "isPrime", "number" => number}} when is_number(number) -> 67 | Logger.debug("Received valid request for number: #{number}") 68 | response = %{"method" => "isPrime", "prime" => prime?(number)} 69 | :gen_tcp.send(socket, [Jason.encode!(response), ?\n]) 70 | echo_lines_until_closed(socket) 71 | 72 | other -> 73 | Logger.debug("Received invalid request: #{inspect(other)}") 74 | :gen_tcp.send(socket, "malformed request\n") 75 | {:error, :invalid_request} 76 | end 77 | 78 | {:error, :closed} -> 79 | :ok 80 | 81 | {:error, reason} -> 82 | {:error, reason} 83 | end 84 | end 85 | 86 | defp prime?(number) when is_float(number), do: false 87 | defp prime?(number) when number <= 1, do: false 88 | defp prime?(number) when number in [2, 3], do: true 89 | 90 | defp prime?(number) do 91 | not Enum.any?(2..trunc(:math.sqrt(number)), &(rem(number, &1) == 0)) 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/lib/protohackers/udp_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.UDPServer do 2 | use GenServer 3 | 4 | require Logger 5 | 6 | @spec start_link(keyword()) :: GenServer.on_start() 7 | def start_link(opts) do 8 | GenServer.start_link(__MODULE__, opts) 9 | end 10 | 11 | defstruct [:socket, store: %{"version" => "Protohackers in Elixir 1.0"}] 12 | 13 | @impl true 14 | def init(opts) do 15 | port = Keyword.fetch!(opts, :port) 16 | 17 | address = 18 | case System.fetch_env("FLY_APP_NAME") do 19 | {:ok, _} -> 20 | {:ok, fly_global_ip} = :inet.getaddr(~c"fly-global-services", :inet) 21 | fly_global_ip 22 | 23 | :error -> 24 | {0, 0, 0, 0} 25 | end 26 | 27 | Logger.info("Started server on #{:inet.ntoa(address)}:#{port}") 28 | 29 | case :gen_udp.open(5005, [:binary, active: false, recbuf: 1000, ip: address]) do 30 | {:ok, socket} -> 31 | state = %__MODULE__{socket: socket} 32 | {:ok, state, {:continue, :recv}} 33 | 34 | {:error, reason} -> 35 | {:stop, reason} 36 | end 37 | end 38 | 39 | @impl true 40 | def handle_continue(:recv, %__MODULE__{} = state) do 41 | case :gen_udp.recv(state.socket, 0) do 42 | {:ok, {address, port, packet}} -> 43 | Logger.debug( 44 | "Received UDP packet from #{inspect(address)}:#{inspect(port)}: #{inspect(packet)}" 45 | ) 46 | 47 | state = 48 | case String.split(packet, "=", parts: 2) do 49 | # Don't do anything for the "version" key. 50 | ["version", _value] -> 51 | state 52 | 53 | [key, value] -> 54 | Logger.debug("Inserted key #{inspect(key)} with value #{inspect(value)}") 55 | put_in(state.store[key], value) 56 | 57 | [key] -> 58 | Logger.debug("Requested key: #{inspect(key)}") 59 | packet = "#{key}=#{state.store[key]}" 60 | :gen_udp.send(state.socket, address, port, packet) 61 | state 62 | end 63 | 64 | {:noreply, state, {:continue, :recv}} 65 | 66 | {:error, reason} -> 67 | {:stop, reason} 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ProtohackersFirstDays.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :protohackers_first_days, 7 | version: "0.1.0", 8 | elixir: "~> 1.14", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger], 18 | mod: {Protohackers.Application, []} 19 | ] 20 | end 21 | 22 | # Run "mix help deps" to learn about dependencies. 23 | defp deps do 24 | [ 25 | {:jason, "~> 1.4"} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, 3 | } 4 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/rel/env.bat.eex: -------------------------------------------------------------------------------- 1 | @echo off 2 | rem Set the release to load code on demand (interactive) instead of preloading (embedded). 3 | rem set RELEASE_MODE=interactive 4 | 5 | rem Set the release to work across nodes. 6 | rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none". 7 | rem set RELEASE_DISTRIBUTION=name 8 | rem set RELEASE_NODE=<%= @release.name %> 9 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/rel/env.sh.eex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ip=$(grep fly-local-6pn /etc/hosts | cut -f 1) 4 | export RELEASE_DISTRIBUTION=name 5 | export RELEASE_NODE=$FLY_APP_NAME@"$ip" 6 | export ELIXIR_ERL_OPTIONS="-proto_dist inet6_tcp" 7 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/rel/remote.vm.args.eex: -------------------------------------------------------------------------------- 1 | ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html 2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here 3 | 4 | ## Increase number of concurrent ports/sockets 5 | ##+Q 65536 6 | 7 | ## Tweak GC to run more often 8 | ##-env ERL_FULLSWEEP_AFTER 10 9 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/rel/vm.args.eex: -------------------------------------------------------------------------------- 1 | ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html 2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here 3 | 4 | ## Increase number of concurrent ports/sockets 5 | ##+Q 65536 6 | 7 | ## Tweak GC to run more often 8 | ##-env ERL_FULLSWEEP_AFTER 10 9 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/test/protohackers/budget_chat_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.BudgetChatServerTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "whole flow" do 5 | {:ok, socket1} = 6 | :gen_tcp.connect(~c"localhost", 5004, mode: :binary, active: false, packet: :line) 7 | 8 | {:ok, socket2} = 9 | :gen_tcp.connect(~c"localhost", 5004, mode: :binary, active: false, packet: :line) 10 | 11 | assert {:ok, "What's your username?\n"} = :gen_tcp.recv(socket1, 0, 5_000) 12 | :ok = :gen_tcp.send(socket1, "Sock1\n") 13 | assert {:ok, "* The room contains: \n"} = :gen_tcp.recv(socket1, 0, 5_000) 14 | 15 | assert {:ok, "What's your username?\n"} = :gen_tcp.recv(socket2, 0, 5_000) 16 | 17 | :ok = :gen_tcp.send(socket2, "Sock2\n") 18 | assert {:ok, "* The room contains: Sock1\n"} = :gen_tcp.recv(socket2, 0, 5_000) 19 | assert {:ok, "* Sock2 has entered the chat\n"} = :gen_tcp.recv(socket1, 0, 5_000) 20 | 21 | :ok = :gen_tcp.send(socket1, "Hello world!\n") 22 | assert {:ok, "[Sock1] Hello world!\n"} = :gen_tcp.recv(socket2, 0, 5_000) 23 | 24 | :ok = :gen_tcp.send(socket2, "Hi to you!\n") 25 | assert {:ok, "[Sock2] Hi to you!\n"} = :gen_tcp.recv(socket1, 0, 5_000) 26 | 27 | :gen_tcp.close(socket2) 28 | 29 | assert {:ok, "* Sock2 left\n"} = :gen_tcp.recv(socket1, 0, 5_000) 30 | 31 | {:ok, socket3} = 32 | :gen_tcp.connect(~c"localhost", 5004, mode: :binary, active: false, packet: :line) 33 | 34 | assert {:ok, "What's your username?\n"} = :gen_tcp.recv(socket3, 0, 5_000) 35 | :ok = :gen_tcp.send(socket3, "Sock3\n") 36 | assert {:ok, "* The room contains: Sock1\n"} = :gen_tcp.recv(socket3, 0, 5_000) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/test/protohackers/echo_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.EchoServerTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "echoes anything back" do 5 | {:ok, socket} = :gen_tcp.connect(~c"localhost", 5001, mode: :binary, active: false) 6 | assert :gen_tcp.send(socket, "foo") == :ok 7 | assert :gen_tcp.send(socket, "bar") == :ok 8 | :gen_tcp.shutdown(socket, :write) 9 | assert :gen_tcp.recv(socket, 0, 5000) == {:ok, "foobar"} 10 | end 11 | 12 | @tag :capture_log 13 | test "echo server has a max buffer size" do 14 | {:ok, socket} = :gen_tcp.connect(~c"localhost", 5001, mode: :binary, active: false) 15 | assert :gen_tcp.send(socket, :binary.copy("a", 1024 * 100 + 1)) == :ok 16 | assert :gen_tcp.recv(socket, 0) == {:error, :closed} 17 | end 18 | 19 | test "handles multiple concurrent connections" do 20 | tasks = 21 | for _ <- 1..4 do 22 | Task.async(fn -> 23 | {:ok, socket} = :gen_tcp.connect(~c"localhost", 5001, mode: :binary, active: false) 24 | assert :gen_tcp.send(socket, "foo") == :ok 25 | assert :gen_tcp.send(socket, "bar") == :ok 26 | :gen_tcp.shutdown(socket, :write) 27 | assert :gen_tcp.recv(socket, 0, 5000) == {:ok, "foobar"} 28 | end) 29 | end 30 | 31 | Enum.each(tasks, &Task.await/1) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/test/protohackers/mitm/boguscoin_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.MIMT.BoguscoinTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Protohackers.MITM.Boguscoin 5 | 6 | @tonys_address "7YWHMfk9JZe0LM0g1ZauHuiSxhI" 7 | 8 | @sample_addresses [ 9 | "7F1u3wSD5RbOHQmupo9nx4TnhQ", 10 | "7iKDZEwPZSqIvDnHvVN2r0hUWXD5rHX", 11 | "7LOrwbDlS8NujgjddyogWgIM93MV5N2VR", 12 | "7adNeSwJkMakpEcln9HEtthSRtxdmEHOT8T" 13 | ] 14 | 15 | describe "rewrite_addresses/1" do 16 | test "doesn't do anything if there are no addresses" do 17 | assert Boguscoin.rewrite_addresses("hello") == "hello" 18 | end 19 | 20 | test "ignores too-long addresses" do 21 | str = "This is too long: 7xtGNgS2V2d32VsCXYpZSdQXOY4Iy7vlVboZ" 22 | assert Boguscoin.rewrite_addresses(str) == str 23 | end 24 | 25 | test "ignores suspicious addresses" do 26 | str = "Not Boguscoin: 7cuyhvvuR2kduZsmZNmBqmUJpZTjMDbhtsD-PI1NrEdUAZ8Ar5NX0DUTdCUEo55V-1234" 27 | 28 | assert Boguscoin.rewrite_addresses(str) == str 29 | end 30 | 31 | test "multiple addresses" do 32 | assert Boguscoin.rewrite_addresses(Enum.join(@sample_addresses, " ")) == 33 | Enum.join( 34 | List.duplicate( 35 | @tonys_address, 36 | length(@sample_addresses) 37 | ), 38 | " " 39 | ) 40 | end 41 | 42 | test "with sample addresses" do 43 | for address <- @sample_addresses do 44 | assert Boguscoin.rewrite_addresses(address) == @tonys_address 45 | assert Boguscoin.rewrite_addresses(address <> " foo") == @tonys_address <> " foo" 46 | assert Boguscoin.rewrite_addresses("foo " <> address) == "foo " <> @tonys_address 47 | 48 | assert Boguscoin.rewrite_addresses("foo " <> address <> " bar") == 49 | "foo " <> @tonys_address <> " bar" 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/test/protohackers/prices_server/db_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.PricesServer.DBTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Protohackers.PricesServer.DB 5 | 6 | test "adding elements and getting the average" do 7 | db = DB.new() 8 | 9 | assert DB.query(db, 0, 100) == 0 10 | 11 | db = 12 | db 13 | |> DB.add(1, 10) 14 | |> DB.add(2, 20) 15 | |> DB.add(3, 30) 16 | 17 | assert DB.query(db, 0, 100) == 20 18 | assert DB.query(db, 0, 2) == 15 19 | assert DB.query(db, 2, 3) == 25 20 | assert DB.query(db, 4, 100) == 0 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/test/protohackers/prices_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.PricesServerTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "handles queries" do 5 | {:ok, socket} = :gen_tcp.connect(~c"localhost", 5003, mode: :binary, active: false) 6 | 7 | :ok = :gen_tcp.send(socket, <>) 8 | :ok = :gen_tcp.send(socket, <>) 9 | :ok = :gen_tcp.send(socket, <>) 10 | 11 | :ok = :gen_tcp.send(socket, <>) 12 | assert {:ok, <<2::32-signed-big>>} = :gen_tcp.recv(socket, 4, 10_000) 13 | end 14 | 15 | test "handles clients separately" do 16 | {:ok, socket1} = :gen_tcp.connect(~c"localhost", 5003, mode: :binary, active: false) 17 | {:ok, socket2} = :gen_tcp.connect(~c"localhost", 5003, mode: :binary, active: false) 18 | 19 | :ok = :gen_tcp.send(socket1, <>) 20 | :ok = :gen_tcp.send(socket2, <>) 21 | 22 | :ok = :gen_tcp.send(socket1, <>) 23 | assert {:ok, <<1::32-signed-big>>} = :gen_tcp.recv(socket1, 4, 10_000) 24 | 25 | :ok = :gen_tcp.send(socket2, <>) 26 | assert {:ok, <<2::32-signed-big>>} = :gen_tcp.recv(socket2, 4, 10_000) 27 | end 28 | 29 | test "at least five simultaneous clients are supported" do 30 | 0..50 31 | |> Enum.map(fn _ -> 32 | Task.async(fn -> 33 | {:ok, socket} = :gen_tcp.connect(~c"localhost", 5003, mode: :binary, active: false) 34 | :ok = :gen_tcp.send(socket, <>) 35 | :ok = :gen_tcp.send(socket, <>) 36 | :ok = :gen_tcp.send(socket, <>) 37 | :ok = :gen_tcp.send(socket, <>) 38 | :ok = :gen_tcp.send(socket, <>) 39 | 40 | assert :gen_tcp.recv(socket, 0) == {:ok, <<101::32-signed-big>>} 41 | 42 | :ok = :gen_tcp.close(socket) 43 | end) 44 | end) 45 | |> Task.await_many() 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/test/protohackers/prime_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.PrimeServerTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "echoes back JSON" do 5 | {:ok, socket} = :gen_tcp.connect(~c"localhost", 5002, mode: :binary, active: false) 6 | :gen_tcp.send(socket, Jason.encode!(%{method: "isPrime", number: 7}) <> "\n") 7 | 8 | assert {:ok, data} = :gen_tcp.recv(socket, 0, 5000) 9 | assert String.ends_with?(data, "\n") 10 | assert Jason.decode!(data) == %{"method" => "isPrime", "prime" => true} 11 | 12 | :gen_tcp.send(socket, Jason.encode!(%{method: "isPrime", number: 6}) <> "\n") 13 | 14 | assert {:ok, data} = :gen_tcp.recv(socket, 0, 5000) 15 | assert String.ends_with?(data, "\n") 16 | assert Jason.decode!(data) == %{"method" => "isPrime", "prime" => false} 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/test/protohackers/udp_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.UDPServerTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "insert and retrieve requests" do 5 | {:ok, socket} = :gen_udp.open(0, [:binary, active: false, recbuf: 1000]) 6 | 7 | :ok = :gen_udp.send(socket, {127, 0, 0, 1}, 5005, "foo=1") 8 | :ok = :gen_udp.send(socket, {127, 0, 0, 1}, 5005, "foo") 9 | assert {:ok, {_address, _port, "foo=1"}} = :gen_udp.recv(socket, 0) 10 | 11 | :ok = :gen_udp.send(socket, {127, 0, 0, 1}, 5005, "foo=2") 12 | :ok = :gen_udp.send(socket, {127, 0, 0, 1}, 5005, "foo") 13 | assert {:ok, {_address, _port, "foo=2"}} = :gen_udp.recv(socket, 0) 14 | end 15 | 16 | test "version" do 17 | {:ok, socket} = :gen_udp.open(0, [:binary, active: false, recbuf: 1000]) 18 | 19 | :ok = :gen_udp.send(socket, {127, 0, 0, 1}, 5005, "version=foo") 20 | :ok = :gen_udp.send(socket, {127, 0, 0, 1}, 5005, "version") 21 | 22 | assert {:ok, {_address, _port, "version=Protohackers in Elixir 1.0"}} = 23 | :gen_udp.recv(socket, 0) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /apps/protohackers_first_days/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(assert_receive_timeout: 1_000) 2 | -------------------------------------------------------------------------------- /apps/speed_daemon/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /apps/speed_daemon/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | speed_daemon-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /apps/speed_daemon/lib/speed_daemon/acceptor.ex: -------------------------------------------------------------------------------- 1 | defmodule SpeedDaemon.Acceptor do 2 | use Task, restart: :transient 3 | 4 | require Logger 5 | 6 | @spec start_link(keyword()) :: GenServer.on_start() 7 | def start_link(opts) do 8 | Task.start_link(__MODULE__, :run, [Keyword.fetch!(opts, :port)]) 9 | end 10 | 11 | @spec run(:inet.port_number()) :: no_return() 12 | def run(port) do 13 | case :gen_tcp.listen(port, [ 14 | :binary, 15 | ifaddr: {0, 0, 0, 0}, 16 | active: :once, 17 | reuseaddr: true 18 | ]) do 19 | {:ok, listen_socket} -> 20 | Logger.info("Listening on port #{port}") 21 | accept_loop(listen_socket) 22 | 23 | {:error, reason} -> 24 | raise "failed to listen on port #{port}: #{inspect(reason)}" 25 | end 26 | end 27 | 28 | defp accept_loop(listen_socket) do 29 | case :gen_tcp.accept(listen_socket) do 30 | {:ok, socket} -> 31 | {:ok, _} = SpeedDaemon.ConnectionSupervisor.start_child(socket) 32 | accept_loop(listen_socket) 33 | 34 | {:error, reason} -> 35 | raise "failed to accept connection: #{inspect(reason)}" 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /apps/speed_daemon/lib/speed_daemon/application.ex: -------------------------------------------------------------------------------- 1 | defmodule SpeedDaemon.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | port = String.to_integer(System.get_env("TCP_PORT", "5007")) 11 | 12 | children = [ 13 | {SpeedDaemon.Supervisor, port: port} 14 | ] 15 | 16 | # See https://hexdocs.pm/elixir/Supervisor.html 17 | # for other strategies and supported options 18 | opts = [strategy: :one_for_one, name: SpeedDaemon.Supervisor] 19 | Supervisor.start_link(children, opts) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /apps/speed_daemon/lib/speed_daemon/central_ticket_dispatcher.ex: -------------------------------------------------------------------------------- 1 | defmodule SpeedDaemon.CentralTicketDispatcher do 2 | use GenServer 3 | 4 | alias SpeedDaemon.{DispatchersRegistry, Message} 5 | 6 | require Logger 7 | 8 | def start_link([] = _opts) do 9 | GenServer.start_link(__MODULE__, :no_args, name: __MODULE__) 10 | end 11 | 12 | def add_road(road, speed_limit) do 13 | GenServer.cast(__MODULE__, {:add_road, road, speed_limit}) 14 | end 15 | 16 | def register_observation(road, location, plate, timestamp) do 17 | GenServer.cast(__MODULE__, {:register_observation, road, location, plate, timestamp}) 18 | end 19 | 20 | ## State 21 | 22 | defmodule Road do 23 | defstruct [:id, :speed_limit, observations: %{}, pending_tickets: []] 24 | end 25 | 26 | defstruct roads: %{}, sent_tickets_per_day: [] 27 | 28 | ## Callbacks 29 | 30 | @impl true 31 | def init(:no_args) do 32 | {:ok, %__MODULE__{}} 33 | end 34 | 35 | @impl true 36 | def handle_cast(cast, state) 37 | 38 | def handle_cast({:add_road, road_id, speed_limit}, state) do 39 | Logger.debug("Added road #{road_id} with speed limit #{speed_limit}") 40 | new_road = %Road{id: road_id, speed_limit: speed_limit} 41 | state = update_in(state.roads, &Map.put_new(&1, road_id, new_road)) 42 | {:noreply, state} 43 | end 44 | 45 | def handle_cast({:register_observation, road_id, location, plate, timestamp}, state) do 46 | state = 47 | update_in(state.roads[road_id].observations[plate], fn observations -> 48 | observations = observations || [] 49 | [{timestamp, location}] ++ observations 50 | end) 51 | 52 | road = generate_tickets(state.roads[road_id], plate) 53 | 54 | state = put_in(state.roads[road_id], road) 55 | state = dispatch_tickets_to_available_dispatchers(state, road_id) 56 | {:noreply, state} 57 | end 58 | 59 | @impl true 60 | def handle_info(info, state) 61 | 62 | def handle_info({:register, DispatchersRegistry, road_id, _partition, _value}, state) do 63 | state = dispatch_tickets_to_available_dispatchers(state, road_id) 64 | {:noreply, state} 65 | end 66 | 67 | # We don't need to do anything here. 68 | def handle_info({:unregister, DispatchersRegistry, _dispatcher, _partition}, state) do 69 | {:noreply, state} 70 | end 71 | 72 | ## Helpers 73 | 74 | defp generate_tickets(%Road{} = road, plate) do 75 | observations = 76 | road.observations[plate] 77 | |> Enum.sort_by(fn {timestamp, _location} -> timestamp end) 78 | |> Enum.dedup_by(fn {timestamp, _location} -> timestamp end) 79 | 80 | tickets = 81 | observations 82 | |> Stream.zip(Enum.drop(observations, 1)) 83 | |> Enum.flat_map(fn {{ts1, location1}, {ts2, location2}} -> 84 | distance = abs(location1 - location2) 85 | speed_miles_per_hour = round(distance / (ts2 - ts1) * 3600) 86 | 87 | if speed_miles_per_hour > road.speed_limit do 88 | [ 89 | %Message.Ticket{ 90 | plate: plate, 91 | road: road.id, 92 | mile1: location1, 93 | timestamp1: ts1, 94 | mile2: location2, 95 | timestamp2: ts2, 96 | speed: speed_miles_per_hour * 100 97 | } 98 | ] 99 | else 100 | [] 101 | end 102 | end) 103 | 104 | %Road{road | pending_tickets: road.pending_tickets ++ tickets} 105 | end 106 | 107 | defp dispatch_tickets_to_available_dispatchers(state, road_id) do 108 | case Map.fetch(state.roads, road_id) do 109 | {:ok, %Road{} = road} -> 110 | {tickets_left_to_dispatch, sent_tickets_per_day} = 111 | Enum.flat_map_reduce( 112 | state.roads[road_id].pending_tickets, 113 | state.sent_tickets_per_day, 114 | fn ticket, acc -> 115 | case Registry.lookup(DispatchersRegistry, road.id) do 116 | [] -> 117 | Logger.debug("No dispatchers available for road #{ticket.road}, keeping ticket") 118 | {[ticket], acc} 119 | 120 | dispatchers -> 121 | ticket_start_day = floor(ticket.timestamp1 / 86_400) 122 | ticket_end_day = floor(ticket.timestamp2 / 86_400) 123 | 124 | if {ticket_start_day, ticket.plate} in acc or 125 | {ticket_end_day, ticket.plate} in acc do 126 | Logger.debug( 127 | "Not sending ticket because it was already sent for this day: #{inspect(ticket)}" 128 | ) 129 | 130 | {[], acc} 131 | else 132 | {pid, _} = Enum.random(dispatchers) 133 | GenServer.cast(pid, {:dispatch_ticket, ticket}) 134 | 135 | sent = for day <- ticket_start_day..ticket_end_day, do: {day, ticket.plate} 136 | {[], acc ++ sent} 137 | end 138 | end 139 | end 140 | ) 141 | 142 | state = put_in(state.sent_tickets_per_day, sent_tickets_per_day) 143 | state = put_in(state.roads[road_id].pending_tickets, tickets_left_to_dispatch) 144 | state 145 | 146 | :error -> 147 | state 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /apps/speed_daemon/lib/speed_daemon/connection.ex: -------------------------------------------------------------------------------- 1 | defmodule SpeedDaemon.Connection do 2 | use GenServer, restart: :temporary 3 | 4 | alias SpeedDaemon.{CentralTicketDispatcher, DispatchersRegistry, Message} 5 | 6 | require Logger 7 | 8 | def start_link(socket) do 9 | GenServer.start_link(__MODULE__, socket) 10 | end 11 | 12 | defstruct [:socket, :type, :heartbeat_ref, buffer: <<>>] 13 | 14 | @impl true 15 | def init(socket) do 16 | Logger.debug("Client connected") 17 | {:ok, %__MODULE__{socket: socket}} 18 | end 19 | 20 | @impl true 21 | def handle_info(message, state) 22 | 23 | def handle_info({:tcp, socket, data}, %__MODULE__{socket: socket} = state) do 24 | state = update_in(state.buffer, &(&1 <> data)) 25 | :ok = :inet.setopts(socket, active: :once) 26 | parse_all_data(state) 27 | end 28 | 29 | def handle_info({:tcp_error, socket, reason}, %__MODULE__{socket: socket} = state) do 30 | Logger.error("Connection closed because of error: #{inspect(reason)}") 31 | {:stop, :normal, state} 32 | end 33 | 34 | def handle_info({:tcp_closed, socket}, %__MODULE__{socket: socket} = state) do 35 | Logger.debug("Connection closed by client") 36 | {:stop, :normal, state} 37 | end 38 | 39 | def handle_info(:send_heartbeat, %__MODULE__{} = state) do 40 | send_message(state, %Message.Heartbeat{}) 41 | {:noreply, state} 42 | end 43 | 44 | @impl true 45 | def handle_cast({:dispatch_ticket, ticket}, %__MODULE__{type: %Message.IAmDispatcher{}} = state) do 46 | send_message(state, ticket) 47 | {:noreply, state} 48 | end 49 | 50 | ## Helpers 51 | 52 | defp send_message(%__MODULE__{socket: socket}, message) do 53 | Logger.debug("Sending message: #{inspect(message)}") 54 | :gen_tcp.send(socket, Message.encode(message)) 55 | end 56 | 57 | defp parse_all_data(%__MODULE__{} = state) do 58 | case Message.decode(state.buffer) do 59 | {:ok, message, rest} -> 60 | Logger.debug("Received message: #{inspect(message)}") 61 | state = put_in(state.buffer, rest) 62 | 63 | case handle_message(state, message) do 64 | {:ok, state} -> 65 | parse_all_data(state) 66 | 67 | {:error, message} -> 68 | send_message(state, %Message.Error{message: message}) 69 | {:stop, :normal, state} 70 | end 71 | 72 | :incomplete -> 73 | {:noreply, state} 74 | 75 | :error -> 76 | send_message(state, %Message.Error{message: "Invalid protocol message"}) 77 | {:stop, :normal, state} 78 | end 79 | end 80 | 81 | defp handle_message( 82 | %__MODULE__{type: %Message.IAmCamera{} = camera} = state, 83 | %Message.Plate{} = message 84 | ) do 85 | CentralTicketDispatcher.register_observation( 86 | camera.road, 87 | camera.mile, 88 | message.plate, 89 | message.timestamp 90 | ) 91 | 92 | {:ok, state} 93 | end 94 | 95 | defp handle_message(%__MODULE__{type: _other_type}, %Message.Plate{}) do 96 | {:error, "Plate messages are only accepted from cameras"} 97 | end 98 | 99 | defp handle_message(state, %Message.WantHeartbeat{interval: interval}) do 100 | interval_in_ms = interval * 100 101 | 102 | if state.heartbeat_ref do 103 | :timer.cancel(state.heartbeat_ref) 104 | end 105 | 106 | if interval > 0 do 107 | {:ok, heartbeat_ref} = :timer.send_interval(interval_in_ms, :send_heartbeat) 108 | {:ok, %__MODULE__{state | heartbeat_ref: heartbeat_ref}} 109 | else 110 | {:ok, %__MODULE__{state | heartbeat_ref: nil}} 111 | end 112 | end 113 | 114 | defp handle_message(%__MODULE__{type: nil} = state, %Message.IAmCamera{} = message) do 115 | CentralTicketDispatcher.add_road(message.road, message.limit) 116 | Logger.metadata(type: :camera, road: message.road, mile: message.mile) 117 | 118 | {:ok, %__MODULE__{state | type: message}} 119 | end 120 | 121 | defp handle_message(%__MODULE__{type: _other}, %Message.IAmCamera{}) do 122 | {:error, "Already registered as a dispatcher or a camera"} 123 | end 124 | 125 | defp handle_message(%__MODULE__{type: nil} = state, %Message.IAmDispatcher{} = message) do 126 | Enum.each(message.roads, fn road -> 127 | {:ok, _} = Registry.register(DispatchersRegistry, road, :unused_value) 128 | end) 129 | 130 | Logger.metadata(type: :dispatcher) 131 | {:ok, %__MODULE__{state | type: message}} 132 | end 133 | 134 | defp handle_message(%__MODULE__{type: _other}, %Message.IAmDispatcher{}) do 135 | {:error, "Already registered as a dispatcher or a camera"} 136 | end 137 | 138 | defp handle_message(%__MODULE__{}, _message) do 139 | {:error, "Invalid message"} 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /apps/speed_daemon/lib/speed_daemon/connection_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule SpeedDaemon.ConnectionSupervisor do 2 | use DynamicSupervisor 3 | 4 | @spec start_link(keyword()) :: Supervisor.on_start() 5 | def start_link([] = _opts) do 6 | DynamicSupervisor.start_link(__MODULE__, :no_args, name: __MODULE__) 7 | end 8 | 9 | @spec start_child(:gen_tcp.socket()) :: DynamicSupervisor.on_start_child() 10 | def start_child(socket) do 11 | child_spec = {SpeedDaemon.Connection, socket} 12 | 13 | with {:ok, conn} <- DynamicSupervisor.start_child(__MODULE__, child_spec), 14 | :ok <- :gen_tcp.controlling_process(socket, conn) do 15 | {:ok, conn} 16 | end 17 | end 18 | 19 | @impl true 20 | def init(:no_args) do 21 | DynamicSupervisor.init(strategy: :one_for_one, max_children: 1000) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /apps/speed_daemon/lib/speed_daemon/message.ex: -------------------------------------------------------------------------------- 1 | defmodule SpeedDaemon.Message do 2 | # Client -> server 3 | 4 | defmodule Plate do 5 | defstruct [:plate, :timestamp] 6 | end 7 | 8 | defmodule WantHeartbeat do 9 | defstruct [:interval] 10 | end 11 | 12 | defmodule IAmCamera do 13 | defstruct [:road, :mile, :limit] 14 | end 15 | 16 | defmodule IAmDispatcher do 17 | defstruct [:roads] 18 | end 19 | 20 | # Server -> client 21 | 22 | defmodule Error do 23 | defstruct [:message] 24 | end 25 | 26 | defmodule Ticket do 27 | defstruct [:plate, :road, :mile1, :timestamp1, :mile2, :timestamp2, :speed] 28 | end 29 | 30 | defmodule Heartbeat do 31 | defstruct [] 32 | end 33 | 34 | ## Functions 35 | 36 | @type_bytes [0x20, 0x40, 0x80, 0x01, 0x02, 0x04, 0x08] 37 | 38 | ## Decoding 39 | 40 | # Plate 41 | def decode(<<0x20, plate_size::8, plate::binary-size(plate_size), timestamp::32, rest::binary>>) do 42 | message = %Plate{plate: plate, timestamp: timestamp} 43 | {:ok, message, rest} 44 | end 45 | 46 | # WantHeartbeat 47 | def decode(<<0x40, interval::32, rest::binary>>) do 48 | {:ok, %WantHeartbeat{interval: interval}, rest} 49 | end 50 | 51 | # IAmCamera 52 | def decode(<<0x80, road::16, mile::16, limit::16, rest::binary>>) do 53 | {:ok, %IAmCamera{road: road, mile: mile, limit: limit}, rest} 54 | end 55 | 56 | # IAmDispatcher 57 | def decode(<<0x81, numroads::8, roads::size(numroads * 2)-binary, rest::binary>>) do 58 | roads = for <>, do: road 59 | {:ok, %IAmDispatcher{roads: roads}, rest} 60 | end 61 | 62 | # Ticket 63 | def decode( 64 | <<0x21, plate_size::8, plate::binary-size(plate_size), road::16, mile1::16, 65 | timestamp1::32, mile2::16, timestamp2::32, speed::16, rest::binary>> 66 | ) do 67 | message = %Ticket{ 68 | plate: plate, 69 | road: road, 70 | mile1: mile1, 71 | timestamp1: timestamp1, 72 | mile2: mile2, 73 | timestamp2: timestamp2, 74 | speed: speed 75 | } 76 | 77 | {:ok, message, rest} 78 | end 79 | 80 | def decode(<<0x41, rest::binary>>) do 81 | {:ok, %Heartbeat{}, rest} 82 | end 83 | 84 | def decode(<<0x10, size::8, message::size(size)-binary, rest::binary>>) do 85 | {:ok, %Error{message: message}, rest} 86 | end 87 | 88 | def decode(<>) when byte in @type_bytes do 89 | :incomplete 90 | end 91 | 92 | def decode(<<_byte, _rest::binary>>) do 93 | :error 94 | end 95 | 96 | def decode(<<>>) do 97 | :incomplete 98 | end 99 | 100 | ## Encoding 101 | 102 | def encode(message) 103 | 104 | def encode(%Error{message: message}) do 105 | <<0x10, byte_size(message)::8-unsigned-big, message::binary>> 106 | end 107 | 108 | def encode(%Plate{} = plate) do 109 | <<0x20, byte_size(plate.plate)::8, plate.plate::binary, plate.timestamp::32>> 110 | end 111 | 112 | def encode(%WantHeartbeat{interval: interval}) do 113 | <<0x40, interval::32>> 114 | end 115 | 116 | def encode(%IAmCamera{road: road, mile: mile, limit: limit}) do 117 | <<0x80, road::16, mile::16, limit::16>> 118 | end 119 | 120 | def encode(%IAmDispatcher{roads: roads}) do 121 | encoded_roads = IO.iodata_to_binary(for road <- roads, do: <>) 122 | <<0x81, length(roads)::8, encoded_roads::binary>> 123 | end 124 | 125 | def encode(%Heartbeat{}) do 126 | <<0x41>> 127 | end 128 | 129 | def encode(%Ticket{} = ticket) do 130 | <<0x21, byte_size(ticket.plate), ticket.plate::binary, ticket.road::16, ticket.mile1::16, 131 | ticket.timestamp1::32, ticket.mile2::16, ticket.timestamp2::32, ticket.speed::16>> 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /apps/speed_daemon/lib/speed_daemon/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule SpeedDaemon.Supervisor do 2 | use Supervisor 3 | 4 | @spec start_link(keyword()) :: Supervisor.on_start() 5 | def start_link(opts) do 6 | Supervisor.start_link(__MODULE__, opts) 7 | end 8 | 9 | @impl true 10 | def init(opts) do 11 | registry_opts = [ 12 | name: SpeedDaemon.DispatchersRegistry, 13 | keys: :duplicate, 14 | listeners: [SpeedDaemon.CentralTicketDispatcher] 15 | ] 16 | 17 | children = [ 18 | {Registry, registry_opts}, 19 | {SpeedDaemon.CentralTicketDispatcher, []}, 20 | {SpeedDaemon.ConnectionSupervisor, []}, 21 | {SpeedDaemon.Acceptor, opts} 22 | ] 23 | 24 | Supervisor.init(children, strategy: :rest_for_one) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /apps/speed_daemon/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule SpeedDaemon.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :speed_daemon, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.14", 13 | start_permanent: Mix.env() == :prod, 14 | deps: deps() 15 | ] 16 | end 17 | 18 | # Run "mix help compile.app" to learn about applications. 19 | def application do 20 | [ 21 | extra_applications: [:logger], 22 | mod: {SpeedDaemon.Application, []} 23 | ] 24 | end 25 | 26 | # Run "mix help deps" to learn about dependencies. 27 | defp deps do 28 | [] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /apps/speed_daemon/mix.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatyouhide/protohackers_in_elixir/f73015610479467c0d60fcaeda6da8dbcc4d6538/apps/speed_daemon/mix.lock -------------------------------------------------------------------------------- /apps/speed_daemon/rel/env.bat.eex: -------------------------------------------------------------------------------- 1 | @echo off 2 | rem Set the release to load code on demand (interactive) instead of preloading (embedded). 3 | rem set RELEASE_MODE=interactive 4 | 5 | rem Set the release to work across nodes. 6 | rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none". 7 | rem set RELEASE_DISTRIBUTION=name 8 | rem set RELEASE_NODE=<%= @release.name %> 9 | -------------------------------------------------------------------------------- /apps/speed_daemon/rel/env.sh.eex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ip=$(grep fly-local-6pn /etc/hosts | cut -f 1) 4 | export RELEASE_DISTRIBUTION=name 5 | export RELEASE_NODE=$FLY_APP_NAME@"$ip" 6 | export ELIXIR_ERL_OPTIONS="-proto_dist inet6_tcp" 7 | -------------------------------------------------------------------------------- /apps/speed_daemon/rel/remote.vm.args.eex: -------------------------------------------------------------------------------- 1 | ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html 2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here 3 | 4 | ## Increase number of concurrent ports/sockets 5 | ##+Q 65536 6 | 7 | ## Tweak GC to run more often 8 | ##-env ERL_FULLSWEEP_AFTER 10 9 | -------------------------------------------------------------------------------- /apps/speed_daemon/rel/vm.args.eex: -------------------------------------------------------------------------------- 1 | ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html 2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here 3 | 4 | ## Increase number of concurrent ports/sockets 5 | ##+Q 65536 6 | 7 | ## Tweak GC to run more often 8 | ##-env ERL_FULLSWEEP_AFTER 10 9 | -------------------------------------------------------------------------------- /apps/speed_daemon/test/speed_daemon/integration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SpeedDaemon.IntegrationTest do 2 | use ExUnit.Case 3 | 4 | alias SpeedDaemon.Message 5 | 6 | test "ticketing a single car" do 7 | {:ok, camera1} = :gen_tcp.connect(~c"localhost", 5007, [:binary, active: true]) 8 | {:ok, camera2} = :gen_tcp.connect(~c"localhost", 5007, [:binary, active: true]) 9 | {:ok, dispatcher} = :gen_tcp.connect(~c"localhost", 5007, [:binary, active: true]) 10 | 11 | send_message(dispatcher, %Message.IAmDispatcher{roads: [582]}) 12 | 13 | send_message(camera1, %Message.IAmCamera{road: 582, mile: 4452, limit: 100}) 14 | send_message(camera1, %Message.Plate{plate: "UK43PKD", timestamp: 203_663}) 15 | 16 | send_message(camera2, %Message.IAmCamera{road: 582, mile: 4462, limit: 100}) 17 | send_message(camera2, %Message.Plate{plate: "UK43PKD", timestamp: 203_963}) 18 | 19 | assert_receive {:tcp, ^dispatcher, data} 20 | assert {:ok, message, <<>>} = Message.decode(data) 21 | 22 | assert message == %Message.Ticket{ 23 | mile1: 4452, 24 | mile2: 4462, 25 | plate: "UK43PKD", 26 | road: 582, 27 | speed: 12000, 28 | timestamp1: 203_663, 29 | timestamp2: 203_963 30 | } 31 | end 32 | 33 | test "pending tickets get flushed" do 34 | {:ok, camera1} = :gen_tcp.connect(~c"localhost", 5007, [:binary, active: true]) 35 | {:ok, camera2} = :gen_tcp.connect(~c"localhost", 5007, [:binary, active: true]) 36 | send_message(camera1, %Message.IAmCamera{road: 582, mile: 4452, limit: 100}) 37 | send_message(camera2, %Message.IAmCamera{road: 582, mile: 4462, limit: 100}) 38 | send_message(camera1, %Message.Plate{plate: "IT43PRC", timestamp: 203_663}) 39 | send_message(camera2, %Message.Plate{plate: "IT43PRC", timestamp: 203_963}) 40 | 41 | # We now have a tickets on road 582, but no dispatcher for it. 42 | 43 | {:ok, dispatcher} = :gen_tcp.connect(~c"localhost", 5007, [:binary, active: true]) 44 | send_message(dispatcher, %Message.IAmDispatcher{roads: [582]}) 45 | 46 | assert_receive {:tcp, ^dispatcher, data} 47 | assert {:ok, message, <<>>} = Message.decode(data) 48 | 49 | assert message == %Message.Ticket{ 50 | mile1: 4452, 51 | mile2: 4462, 52 | plate: "IT43PRC", 53 | road: 582, 54 | speed: 12000, 55 | timestamp1: 203_663, 56 | timestamp2: 203_963 57 | } 58 | end 59 | 60 | defp send_message(socket, message) do 61 | assert :ok = :gen_tcp.send(socket, Message.encode(message)) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /apps/speed_daemon/test/speed_daemon/message_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SpeedDaemon.MessageTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias SpeedDaemon.Message 5 | 6 | describe "encode/1 + decode/1 for all messages" do 7 | test "Plate" do 8 | message = %Message.Plate{plate: "UK43PKD", timestamp: 203_663} 9 | assert {:ok, ^message, ""} = Message.decode(Message.encode(message)) 10 | end 11 | 12 | test "WantHeartbeat" do 13 | message = %Message.WantHeartbeat{interval: 1000} 14 | assert {:ok, ^message, ""} = Message.decode(Message.encode(message)) 15 | end 16 | 17 | test "IAmDispatcher" do 18 | message = %Message.IAmDispatcher{roads: [582]} 19 | assert {:ok, ^message, ""} = Message.decode(Message.encode(message)) 20 | end 21 | 22 | test "IAmCamera" do 23 | message = %Message.IAmCamera{road: 582, mile: 4452, limit: 100} 24 | assert {:ok, ^message, ""} = Message.decode(Message.encode(message)) 25 | end 26 | 27 | test "Error" do 28 | message = %Message.Error{message: "Something went wrong"} 29 | assert {:ok, ^message, ""} = Message.decode(Message.encode(message)) 30 | end 31 | 32 | test "Ticket" do 33 | message = %Message.Ticket{ 34 | mile1: 4452, 35 | mile2: 4462, 36 | plate: "UK43PKD", 37 | road: 582, 38 | speed: 12000, 39 | timestamp1: 203_663, 40 | timestamp2: 203_963 41 | } 42 | 43 | assert {:ok, ^message, ""} = Message.decode(Message.encode(message)) 44 | end 45 | 46 | test "Heartbeat" do 47 | message = %Message.Heartbeat{} 48 | assert {:ok, ^message, ""} = Message.decode(Message.encode(message)) 49 | end 50 | end 51 | 52 | describe "decode/1" do 53 | test "returns :incomplete for valid-looking incomplete messages" do 54 | assert Message.decode(<<>>) == :incomplete 55 | assert Message.decode(<<0x20>>) == :incomplete 56 | end 57 | 58 | test "returns :error for invalid messages" do 59 | assert Message.decode(<<0x00>>) == :error 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /apps/speed_daemon/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | log_level = 4 | if config_env() == :test do 5 | :warn 6 | else 7 | :info 8 | end 9 | 10 | config :logger, level: log_level 11 | config :logger, :console, metadata: [:module, :address, :session, :pid] 12 | -------------------------------------------------------------------------------- /config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | case System.fetch_env("LOG_LEVEL") do 4 | {:ok, level} -> 5 | config :logger, level: String.to_existing_atom(level) 6 | 7 | :error -> 8 | :ok 9 | end 10 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml file generated for protohackers-in-elixir on 2022-12-30T16:09:40+01:00 2 | 3 | app = "protohackers-in-elixir" 4 | kill_signal = "SIGTERM" 5 | kill_timeout = 5 6 | processes = [] 7 | 8 | [env] 9 | LOG_LEVEL = "debug" 10 | TCP_PORT = "5000" 11 | UDP_PORT = "6000" 12 | 13 | [experimental] 14 | allowed_public_ports = [] 15 | auto_rollback = true 16 | 17 | [[services]] 18 | internal_port = 5000 19 | protocol = "tcp" 20 | 21 | [[services.ports]] 22 | handlers = [] 23 | port = 5000 24 | 25 | [[services]] 26 | internal_port = 6000 27 | protocol = "udp" 28 | 29 | [[services.ports]] 30 | handlers = [] 31 | port = 6000 32 | 33 | [services.concurrency] 34 | hard_limit = 250 35 | soft_limit = 200 36 | type = "connections" 37 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Protohackers.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | apps_path: "apps", 7 | version: "0.1.0", 8 | start_permanent: Mix.env() == :prod, 9 | deps: deps(), 10 | releases: [ 11 | protohackers: [ 12 | applications: [protohackers_first_days: :permanent] 13 | ], 14 | speed_daemon: [ 15 | applications: [speed_daemon: :permanent] 16 | ], 17 | line_reversal: [ 18 | applications: [line_reversal: :permanent] 19 | ], 20 | isl: [ 21 | applications: [isl: :permanent] 22 | ] 23 | ] 24 | ] 25 | end 26 | 27 | defp deps do 28 | [] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, 3 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 4 | "thousand_island": {:hex, :thousand_island, "0.6.0", "fb03f42dfc41760127e7c4d5a401fa1980337a22ceafb7d4b5eca918092a4eef", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "79b6a51234a35cdc59308e7286c67d3b20ae2e3d2011554e95d51ba0011c9a33"}, 5 | } 6 | --------------------------------------------------------------------------------