Repository: X-Plane/elixir-raknet
Branch: main
Commit: fee7a4b97eb8
Files: 26
Total size: 114.2 KB
Directory structure:
gitextract_ft9zctf5/
├── .circleci/
│ └── config.yml
├── .formatter.exs
├── .github/
│ └── ISSUE_TEMPLATE/
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .idea/
│ └── runConfigurations/
│ ├── Format.xml
│ └── Test.xml
├── LICENSE
├── README.md
├── config/
│ ├── .credo.exs
│ └── credo_checks/
│ └── explicitly_ignore_return_values.ex
├── lib/
│ ├── application.ex
│ ├── connection.ex
│ ├── message.ex
│ ├── packet.ex
│ ├── reliability_layer.ex
│ ├── server.ex
│ └── system_address.ex
├── mix.exs
├── raknet.iml
└── test/
├── connection_test.exs
├── message_test.exs
├── packet_test.exs
├── server_test.exs
├── system_address_test.exs
└── test_helper.exs
================================================
FILE CONTENTS
================================================
================================================
FILE: .circleci/config.yml
================================================
# Elixir CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-elixir/ for more details
version: 2.1
orbs:
slack: circleci/slack@3.4.2
jobs:
build:
docker:
- image: cimg/elixir:1.14.1
environment: # environment variables for primary container
MIX_ENV: test
SEPARATE_IPV6_PORT: false
resource_class: medium
working_directory: ~/repo
steps:
- run: git clone https://github.com/X-Plane/elixir-raknet.git .
- run: git submodule update --init --remote
- run: mix local.hex --force
- restore_cache: # restores saved mix cache; Read about caching dependencies: https://circleci.com/docs/2.0/caching/
keys: # list of cache keys, in decreasing specificity
- v1-mix-cache-{{ .Branch }}-{{ checksum "mix.lock" }}
- v1-mix-cache-{{ .Branch }}
- v1-mix-cache
- restore_cache: # restores saved build cache
keys:
- v1-build-cache-{{ .Branch }}
- v1-build-cache
- run:
name: Compile
command: mix do deps.get, compile
- save_cache: # generate and store mix cache
key: v1-mix-cache-{{ .Branch }}-{{ checksum "mix.lock" }}
paths: "deps"
- save_cache: # don't forget to save a *build* cache, too
key: v1-build-cache-{{ .Branch }}
paths: "_build"
- run: LOG_LEVEL=warn SEPARATE_IPV6_PORT=false mix test --exclude ipv6:true
- store_test_results: # upload junit test results for display in Test Summary. More info: https://circleci.com/docs/2.0/collect-test-data/
path: _build/test/lib/raknet
- run: mix format --check-formatted
- run: bash -c "mix credo --strict --ignore tagtodo ; if [[ \$? -ge 16 ]] ; then exit 1 ; else exit 0 ; fi"
================================================
FILE: .formatter.exs
================================================
[
line_length: 140,
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior (including a [minimum reproducible example](https://stackoverflow.com/help/minimal-reproducible-example), as appropriate):
1. ...
2. ...
3. ...
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. macOS 10.15.4]
- Elixir Version [e.g. 1.11.1]
- Erlang Version [e.g. 23.0]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .gitignore
================================================
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
raknet-*.tar
================================================
FILE: .idea/runConfigurations/Format.xml
================================================
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Format" type="MixRunConfigurationType" factoryName="Elixir Mix">
<module name="raknet" />
<mix>
<argument>format</argument>
</mix>
<working-directory url="file://$PROJECT_DIR$" />
<envs>
<env name="MIX_ENV" value="test" />
</envs>
<module name="raknet" />
<module-filters inherit-application-module-filters="true" />
<method v="2" />
</configuration>
</component>
================================================
FILE: .idea/runConfigurations/Test.xml
================================================
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test" type="MixExUnitRunConfigurationType" factoryName="Elixir Mix ExUnit">
<module name="raknet" />
<module name="raknet" />
<module-filters inherit-application-module-filters="true" />
<method v="2" />
</configuration>
</component>
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021 X-Plane
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Elixir RakNet
**`main` build status**: [](https://circleci.com/gh/X-Plane/elixir-raknet/tree/main) **Latest commit build status**: [](https://circleci.com/gh/X-Plane/elixir-raknet)
This is an Elixir implementation of the [RakNet](https://github.com/facebookarchive/RakNet)/RakLib networking communication protocol.
It offers things like stateful connections, reliable (or unreliable) UDP transmissions, and client clock synchronization, all of which are generally necessary for implementing a multiplayer game server.
Note that this is not a *complete* implementation of the RakNet protocol—it currently offers only what X-Plane needs for its massive multiplayer server. Known limitations include:
1. The server doesn't do well with retransmitting unacknowledged reliable packets, since we only ever send unreliable packets in our MMO server.
2. We don't support splitting packets larger than the connection's MTU size—this leaves the responsibility on the client to make sure your packets aren't over the size limit (which of course can vary depending on the client's connection—yikes!). In practice, this isn't a problem if you know your packets are reasonably small (well under 1 KB).
We'd welcome well-tested pull requests to fix these things, though. (See the [Contributing](#contributing) section below.)
## Installation
Add `raknet` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:raknet, git: "git://github.com/X-Plane/elixir-raknet.git", branch: "main"},
]
end
```
## Usage
In your application, you'll need two things:
1. A client state struct that implements the `RakNet.Client` protocol. For the X-Plane massive multiplayer server, our state struct looks like this:
defmodule MmoServer.Client.State do
@moduledoc "State for our implementation of RakNet.Client"
@enforce_keys [:session_id, :connection_pid]
defstruct session_id: -1,
connection_pid: nil,
# Other fields, which get default-initialized
# when a new connection is negotiated.
. . .
end
Then, in our implementation of the `RakNet.Client` protocol, we spin up a GenServer for each connection as follows:
defimpl RakNet.Client, for: MmoServer.Client.State do
def new(_client_struct, connection_pid, _) do
new_state = %MmoServer.Client.State{
connection_pid: connection_pid,
session_id: MmoServer.SessionIdServer.new_id()
}
MmoServer.Client.start_link(new_state)
new_state
end
def receive(%MmoServer.Client.State{session_id: id}, packet_type, packet_buffer, transit_time) do
MmoServer.Client.handle_packet(id, packet_type, packet_buffer, transit_time)
end
def got_ack(%MmoServer.Client.State{session_id: _id}, _send_receipt_id) do
# X-Plane doesn't actually do anything with packet acknowledgements
nil
end
def disconnect(%MmoServer.Client.State{session_id: id}) do
MmoServer.Client.disconnect(id)
end
end
2. One or more `RakNet.Server` GenServers (one per port you want to accept connections on). For the X-Plane MMO server, we accept connections on a range of ports, like this:
localhost = {127, 0, 0, 1}
make_server_spec = fn port ->
# Only your client state type and port are required
[MmoServer.Client.State, port, include_timestamp_with_datagrams: true, host: localhost, client_timeout_ms: 20 * 60 * 1_000, open_ipv6_socket: true]
end
servers =
Enum.map(port_min..port_max, fn port ->
{RakNet.Server, make_server_spec.(port)}
end)
opts = [strategy: :one_for_one, name: MmoServer.Supervisor]
Supervisor.start_link(servers, opts)
One configuration option above is worth calling out explicitly: the value of `:open_ipv6_socket` will determine whether we try to open a *separate* socket to receive IPv6 connections, or whether we accept IPv6 connections over the same socket as IPv4. The configuration you need here will depend on your OS configuration, but in general, Linux systems default to sharing a socket, while macOS defaults to using separate sockets. Alternatively, you can set a default value for this using the `SEPARATE_IPV6_PORT` environment variable.
From this point forward, the `RakNet.Server` will create a new stateful `RakNet.Connection` for each RakNet client that connects on your port, and client packets will be forwarded to your client struct.
The client can send messages using the `connection_pid` it was constructed with, like so:
RakNet.Connection.send(state.connection_pid, :reliable, packet)
...where the final argument is a bitstring, and the second argument is the reliability level the packet should be transmitted with. Supported reliability levels are defined in `RakNet.ReliabilityLayer.Reliability` as:
- `:unreliable`
- `:unreliable_sequenced`
- `:reliable`
- `:reliable_ordered`
- `:reliable_sequenced`
- `:unreliable_ack_receipt`
- `:reliable_ack_receipt`
- `:reliable_ordered_ack_receipt`
## Running the tests
You can run the complete unit test suite via the standard `$ mix test` from the top-level directory. Note that you'll see a log message about receiving data before the connection negotiation finished—that's deliberate, since we test handling that error case, but it would be somewhat concerning for it to occur in production.
[We use CircleCI](https://app.circleci.com/pipelines/github/X-Plane/elixir-raknet) to run the test suite on every commit.
You can run the same tests that CircleCI does as follows:
1. Run the Credo linter: `$ mix credo --strict`
2. Confirm the code matches the official formatter: `$ mix format --check-formatted`
3. Confirm the tests pass: `$ mix test` (or if you like more verbose output, `$ mix test --trace`)
## Contributing
Before submitting a pull request, please ensure:
1. You've added appropriate tests for your new changes
2. All tests pass
3. The credo analysis is clean: `$ mix credo --strict --ignore tagtodo`
4. You've run `$ mix format`
================================================
FILE: config/.credo.exs
================================================
# This file contains the configuration for Credo and you are probably reading
# this after creating it with `mix credo.gen.config`.
#
# If you find anything wrong or unclear in this file, please report an
# issue on GitHub: https://github.com/rrrene/credo/issues
#
%{
#
# You can have as many configs as you like in the `configs:` field.
configs: [
%{
#
# Run any exec using `mix credo -C <name>`. If no exec name is given
# "default" is used.
#
name: "default",
#
# These are the files included in the analysis:
files: %{
#
# You can give explicit globs or simply directories.
# In the latter case `**/*.{ex,exs}` will be used.
#
included: ["lib/", "src/", "test/", "web/", "apps/"],
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/", ~r"/data/"]
},
#
# Load and configure plugins here:
#
plugins: [],
#
# If you create your own checks, you must specify the source files for
# them here, so they can be loaded by Credo before running the analysis.
#
requires: [
"config/credo_checks/explicitly_ignore_return_values.ex"
],
#
# If you want to enforce a style guide and need a more traditional linting
# experience, you can change `strict` to `true` below:
#
strict: true,
#
# If you want to use uncolored output by default, you can change `color`
# to `false` below:
#
color: true,
#
# You can customize the parameters of any check by adding a second element
# to the tuple.
#
# To disable a check put `false` as second element:
#
# {Credo.Check.Design.DuplicatedCode, false}
#
checks: [
#
## Consistency Checks
#
{Credo.Check.Consistency.ExceptionNames, []},
{Credo.Check.Consistency.LineEndings, []},
{Credo.Check.Consistency.ParameterPatternMatching, []},
{Credo.Check.Consistency.SpaceAroundOperators, []},
{Credo.Check.Consistency.SpaceInParentheses, []},
{Credo.Check.Consistency.TabsOrSpaces, []},
#
## Design Checks
#
# You can customize the priority of any check
# Priority values are: `low, normal, high, higher`
#
{Credo.Check.Design.AliasUsage, [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
# Don't cause to-do comments to fail
{Credo.Check.Design.TagTODO, [exit_status: 0]},
{Credo.Check.Design.TagFIXME, []},
#
## Readability Checks
#
{Credo.Check.Readability.AliasOrder, []},
{Credo.Check.Readability.FunctionNames, []},
{Credo.Check.Readability.LargeNumbers, []},
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 140]},
{Credo.Check.Readability.ModuleAttributeNames, []},
{Credo.Check.Readability.ModuleDoc, []},
{Credo.Check.Readability.ModuleNames, []},
{Credo.Check.Readability.ParenthesesInCondition, []},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
{Credo.Check.Readability.PredicateFunctionNames, []},
{Credo.Check.Readability.PreferImplicitTry, []},
{Credo.Check.Readability.RedundantBlankLines, []},
{Credo.Check.Readability.Semicolons, []},
{Credo.Check.Readability.SpaceAfterCommas, []},
{Credo.Check.Readability.StringSigils, []},
{Credo.Check.Readability.TrailingBlankLine, []},
{Credo.Check.Readability.TrailingWhiteSpace, []},
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
{Credo.Check.Readability.VariableNames, []},
#
## Refactoring Opportunities
#
{Credo.Check.Refactor.CondStatements, []},
{Credo.Check.Refactor.CyclomaticComplexity, []},
{Credo.Check.Refactor.FunctionArity, []},
{Credo.Check.Refactor.LongQuoteBlocks, []},
{Credo.Check.Refactor.MatchInCondition, []},
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
{Credo.Check.Refactor.Nesting, [max_nesting: 3]},
{Credo.Check.Refactor.UnlessWithElse, []},
{Credo.Check.Refactor.WithClauses, []},
#
## Warnings
#
{Credo.Check.Warning.BoolOperationOnSameValues, []},
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
{Credo.Check.Warning.IExPry, []},
{Credo.Check.Warning.IoInspect, []},
{Credo.Check.Warning.MixEnv, []},
{Credo.Check.Warning.OperationOnSameValues, []},
{Credo.Check.Warning.OperationWithConstantResult, []},
{Credo.Check.Warning.RaiseInsideRescue, []},
{Credo.Check.Warning.UnusedEnumOperation, []},
{Credo.Check.Warning.UnusedFileOperation, []},
{Credo.Check.Warning.UnusedKeywordOperation, []},
{Credo.Check.Warning.UnusedListOperation, []},
{Credo.Check.Warning.UnusedPathOperation, []},
{Credo.Check.Warning.UnusedRegexOperation, []},
{Credo.Check.Warning.UnusedStringOperation, []},
{Credo.Check.Warning.UnusedTupleOperation, []},
#
# Controversial and experimental checks (opt-in, just replace `false` with `[]`)
#
{Credo.Check.Consistency.MultiAliasImportRequireUse, []},
# Tyler says: I don't like this one... it just enforces that you always name your unused vars the *same* way
# either anonymously (like `_`) or with a real name (like `_client`)
{Credo.Check.Consistency.UnusedVariableNames, false},
# Tyler says: I like this one
{Credo.Check.Design.DuplicatedCode, []},
{Credo.Check.Readability.AliasAs, []},
{Credo.Check.Readability.MultiAlias, []},
{Credo.Check.Readability.Specs, false},
{Credo.Check.Readability.SeparateAliasRequire, false},
{Credo.Check.Readability.SinglePipe, []},
{Credo.Check.Readability.WithCustomTaggedTuple, []},
{Credo.Check.Refactor.ABCSize, false},
{Credo.Check.Refactor.AppendSingleItem, []},
{Credo.Check.Refactor.DoubleBooleanNegation, []},
{Credo.Check.Refactor.ModuleDependencies, false},
{Credo.Check.Refactor.PipeChainStart, false},
# Tyler says: I like this one
{Credo.Check.Refactor.VariableRebinding, []},
# Tyler says: I like this one
{Credo.Check.Warning.MapGetUnsafePass, []},
{Credo.Check.Warning.UnsafeToAtom, []},
#
# Custom checks can be created using `mix credo.gen.check`.
#
{ExplicitlyIgnoreReturnValues,
[
ignore: [
{[:Appsignal, :Transaction], :finish},
{[:Appsignal, :Transaction], :complete},
{[:HTTPoison], :start},
{[:RakNet, :Connection], :handle_message},
{[:RakNet, :Connection], :stop},
{[:RakNet, :Client], :got_ack},
{[:RakNet, :Client], :receive},
{[:RakNet, :Client], :disconnect},
{[:MmoServer, :Dsf], :remove},
{[:MmoServer, :Dsf], :update},
{[:MmoServer, :WorldCache], :put},
{[:MmoServer, :WorldCache], :drop},
{[:MmoServer, :SessionIdServer], :return_id}
]
]}
]
}
]
}
================================================
FILE: config/credo_checks/explicitly_ignore_return_values.ex
================================================
defmodule ExplicitlyIgnoreReturnValues do
@moduledoc """
This is a horrifying hack.
It is a complete copy & paste of Credo's stock unused_operation and unused_function_return_helper,
but modified slightly to reverse the logic; instead of specifying modules and types to warn about when unused,
we specify an "allow list" of the *only* modules & functions to allow ignoring.
See original copypasta source here:
https://github.com/rrrene/credo/blob/master/lib/credo/check/warning/unused_operation.ex
https://github.com/rrrene/credo/blob/master/lib/credo/check/warning/unused_function_return_helper.ex
"""
@funs_to_allow_ignoring [
{[:Enum], :each},
{[:GenServer], :cast},
{[:GenServer], :stop},
{[:IO], :inspect},
{[:IO], :puts},
{[:IO], :warn},
{[:IO], :write},
{[:Logger], :configure},
{[:Logger], :debug},
{[:Logger], :error},
{[:Logger], :flush},
{[:Logger], :info},
{[:Logger], :log},
{[:Logger], :warn},
{[:Process], :cancel_timer},
{[:Process], :exit},
{[:Process], :flag},
{[:Process], :hibernate},
{[:Process], :register},
{[:Process], :sleep},
{[:Process], :unlink},
{[:Process], :unregister},
{[:Registry], :register},
{[:Registry], :unregister},
{[:Supervisor], :stop}
]
@block_ops_with_head_expr [:if, :unless, :case, :for, :quote]
use Credo.Check, base_priority: :high, category: :warning
def explanations do
[
check: @moduledoc,
params: [
ignore:
"A list of function AST names whose return values we should allow ignoring. " <>
"Elements of the list should look like: {[:Enum], :each} or {[:Custom, :Name, :Here], :function}"
]
]
end
def param_defaults, do: [ignore: []]
def run(source_file, params \\ []) do
issue_meta = Credo.IssueMeta.for(source_file, params)
funs_to_ignore = @funs_to_allow_ignoring ++ Keyword.fetch!(params, :ignore)
Enum.reduce(find_unused_calls(source_file, params, funs_to_ignore), [], fn invalid_call, issues ->
{_, meta, _} = invalid_call
trigger =
invalid_call
|> Macro.to_string()
|> String.split("(")
|> List.first()
# credo:disable-for-next-line
issues ++ [issue_for(&format_issue/2, issue_meta, meta[:line], trigger)]
end)
end
defp issue_for(format_issue_fun, issue_meta, line_no, trigger) do
format_issue_fun.(
issue_meta,
message: "There should be no unused return values.",
trigger: trigger,
line_no: line_no
)
end
@def_ops [:def, :defp, :defmacro]
@block_ops_with_head_expr [:if, :unless, :case, :for, :quote]
alias Credo.Code.Block
alias Credo.SourceFile
def find_unused_calls(%SourceFile{} = source_file, _params, funs_to_allow_ignoring) do
Credo.Code.prewalk(source_file, &traverse_defs(&1, &2, funs_to_allow_ignoring))
end
for op <- @def_ops do
defp traverse_defs({unquote(op), _meta, arguments} = ast, acc, functions) when is_list(arguments) do
candidates = Credo.Code.prewalk(ast, &find_candidates(&1, &2, functions))
if Enum.any?(candidates) do
{nil, acc ++ filter_unused_calls(ast, candidates)}
else
{ast, acc}
end
end
end
defp traverse_defs(ast, acc, _) do
{ast, acc}
end
#
defp find_candidates({{:., _, [{:__aliases__, _, modules}, function]}, _, _} = ast, acc, functions) do
if {modules, function} in functions do
{ast, acc}
else
# credo:disable-for-next-line
{ast, acc ++ [ast]}
end
end
defp find_candidates(ast, acc, _) do
{ast, acc}
end
# TODO: Everything below this line is unmodified from unused_function_return_helper.ex. If these functions are ever made public, use them.
defp filter_unused_calls(ast, candidates) do
candidates
|> Enum.map(&detect_unused_call(&1, ast))
|> Enum.reject(&is_nil/1)
end
defp detect_unused_call(candidate, ast) do
ast
|> Credo.Code.postwalk(&traverse_verify_candidate(&1, &2, candidate), :not_verified)
|> verified_or_unused_call(candidate)
end
defp verified_or_unused_call(:VERIFIED, _), do: nil
defp verified_or_unused_call(_, candidate), do: candidate
#
defp traverse_verify_candidate(ast, acc, candidate) do
if Credo.Code.contains_child?(ast, candidate) do
verify_candidate(ast, acc, candidate)
else
{ast, acc}
end
end
# we know that `candidate` is part of `ast`
for op <- @def_ops do
defp verify_candidate({unquote(op), _, arguments} = ast, :not_verified = _acc, candidate)
when is_list(arguments) do
# IO.inspect(ast, label: "#{unquote(op)} (#{Macro.to_string(candidate)} #{acc})")
if last_call_in_do_block?(ast, candidate) || last_call_in_rescue_block?(ast, candidate) do
{nil, :VERIFIED}
else
{nil, :FALSIFIED}
end
end
end
defp last_call_in_do_block?(ast, candidate) do
ast
|> Block.calls_in_do_block()
|> List.last()
|> Credo.Code.contains_child?(candidate)
end
defp last_call_in_rescue_block?(ast, candidate) do
ast
|> Block.calls_in_rescue_block()
|> List.last()
|> Credo.Code.contains_child?(candidate)
end
for op <- @block_ops_with_head_expr do
defp verify_candidate({unquote(op), _, arguments} = ast, :not_verified = acc, candidate)
when is_list(arguments) do
# IO.inspect(ast, label: "#{unquote(op)} (#{Macro.to_string(candidate)} #{acc})")
head_expression = Enum.slice(arguments, 0..-2)
if Credo.Code.contains_child?(head_expression, candidate) do
{nil, :VERIFIED}
else
{ast, acc}
end
end
end
defp verify_candidate({:=, _, _} = ast, :not_verified = acc, candidate) do
# IO.inspect(ast, label: ":= (#{Macro.to_string(candidate)} #{acc})")
if Credo.Code.contains_child?(ast, candidate) do
{nil, :VERIFIED}
else
{ast, acc}
end
end
defp verify_candidate(
{:__block__, _, arguments} = ast,
:not_verified = acc,
candidate
)
when is_list(arguments) do
# IO.inspect(ast, label: ":__block__ (#{Macro.to_string(candidate)} #{acc})")
last_call = List.last(arguments)
if Credo.Code.contains_child?(last_call, candidate) do
{ast, acc}
else
{nil, :FALSIFIED}
end
end
defp verify_candidate(
{:|>, _, arguments} = ast,
:not_verified = acc,
candidate
) do
# IO.inspect(ast, label: ":__block__ (#{Macro.to_string(candidate)} #{acc})")
last_call = List.last(arguments)
if Credo.Code.contains_child?(last_call, candidate) do
{ast, acc}
else
{nil, :VERIFIED}
end
end
defp verify_candidate({:->, _, arguments} = ast, :not_verified = acc, _candidate)
when is_list(arguments) do
# IO.inspect(ast, label: ":-> (#{Macro.to_string(ast)} #{acc})")
{ast, acc}
end
defp verify_candidate({:fn, _, arguments} = ast, :not_verified = acc, _candidate)
when is_list(arguments) do
{ast, acc}
end
defp verify_candidate(
{:try, _, arguments} = ast,
:not_verified = acc,
candidate
)
when is_list(arguments) do
# IO.inspect(ast, label: "try (#{Macro.to_string(candidate)} #{acc})")
after_block = Block.after_block_for!(ast)
if after_block && Credo.Code.contains_child?(after_block, candidate) do
{nil, :FALSIFIED}
else
{ast, acc}
end
end
# my_fun()
defp verify_candidate(
{fun_name, _, arguments} = ast,
:not_verified = acc,
candidate
)
when is_atom(fun_name) and is_list(arguments) do
# IO.inspect(ast, label: "my_fun() (#{Macro.to_string(candidate)} #{acc})")
if Credo.Code.contains_child?(arguments, candidate) do
{nil, :VERIFIED}
else
{ast, acc}
end
end
# module.my_fun()
defp verify_candidate(
{{:., _, [{module, _, []}, fun_name]}, _, arguments} = ast,
:not_verified = acc,
candidate
)
when is_atom(fun_name) and is_atom(module) and is_list(arguments) do
# IO.inspect(ast, label: "Mod.fun() (#{Macro.to_string(candidate)} #{acc})")
if Credo.Code.contains_child?(arguments, candidate) do
{nil, :VERIFIED}
else
{ast, acc}
end
end
# :erlang_module.my_fun()
defp verify_candidate(
{{:., _, [module, fun_name]}, _, arguments} = ast,
:not_verified = acc,
candidate
)
when is_atom(fun_name) and is_atom(module) and is_list(arguments) do
# IO.inspect(ast, label: "Mod.fun() (#{Macro.to_string(candidate)} #{acc})")
if Credo.Code.contains_child?(arguments, candidate) do
{nil, :VERIFIED}
else
{ast, acc}
end
end
# MyModule.my_fun()
defp verify_candidate(
{{:., _, [{:__aliases__, _, mods}, fun_name]}, _, arguments} = ast,
:not_verified = acc,
candidate
)
when is_atom(fun_name) and is_list(mods) and is_list(arguments) do
# IO.inspect(ast, label: "Mod.fun() (#{Macro.to_string(candidate)} #{acc})")
if Credo.Code.contains_child?(arguments, candidate) do
{nil, :VERIFIED}
else
{ast, acc}
end
end
defp verify_candidate(ast, acc, _candidate) do
# IO.inspect(ast, label: "_ (#{Macro.to_string(candidate)} #{acc})")
{ast, acc}
end
end
================================================
FILE: lib/application.ex
================================================
defmodule RakNet.Application do
@moduledoc "Supervision tree for RakNet connections"
use Application
def start(_type, _args) do
Logger.configure(
level:
case(System.fetch_env("LOG_LEVEL")) do
{:ok, "debug"} -> :debug
{:ok, "info"} -> :info
{:ok, "warn"} -> :warn
{:ok, "error"} -> :error
_ -> :info
end
)
children = [
{Registry, keys: :unique, name: RakNet.Connection}
]
opts = [strategy: :one_for_one, name: RakNet.Supervisor]
Supervisor.start_link(children, opts)
end
end
================================================
FILE: lib/connection.ex
================================================
defprotocol RakNet.Client do
def new(client_struct, connection_pid, module_data)
def receive(client, packet_type, packet_buffer, time_comp)
def got_ack(client, send_receipt_id)
def disconnect(client)
end
defmodule RakNet.Connection do
@moduledoc "A stateful connection to a client"
use GenServer, restart: :transient
require Logger
alias RakNet.Message
alias RakNet.ReliabilityLayer
alias RakNet.ReliabilityLayer.Reliability
import XUtil.Map
import XUtil.Time, only: [unix_timestamp_ms: 0]
defmodule State do
@moduledoc "The state we pass around within a RakNet.Connection GenServer"
@enforce_keys [
:host,
:port,
:encoded_host,
:client_ips_and_ports,
:encoded_client,
:server_identifier,
:client_module,
:respond,
:base_time
]
defstruct host: nil,
port: nil,
encoded_host: <<>>,
# IPs may be 4 ipv4 octets, or 8 ipv6 hextets
client_ips_and_ports: [],
encoded_client: <<>>,
server_identifier: <<>>,
# The RakNet.Client module that implements your game logic client; this receives game-specific packets and
# uses the respond function we give it to communicate back to the user.
client_module: nil,
# Data you've asked us to pass to your client_module's new/2 factory
client_data: %{},
# The means the RakNet.Server that created us gave us to send a message across the wire to the client
respond: nil,
# The :os.system_time(:millisecond) time at which we were created.
# Use this to get RakNet.Server.timestamp() values relative to creation time
base_time: 0,
# Milliseconds of inactivity before we time out this connection and cause it to self-destruct.
# We count as "activity" a) connected pongs, and b) encapsulated client data packets
# (but not RakNet protocol overhead, like conneciton handshakes)
timeout_ms: 30_000,
# Corresponds to #define INCLUDE_TIMESTAMP_WITH_DATAGRAMS
# (which is in turn enabled by USE_SLIDING_WINDOW_CONGESTION_CONTROL in the RakNetDefinesOverrides.h)
include_timestamp_with_datagrams: false,
# Corresponds to #define MAXIMUM_NUMBER_OF_INTERNAL_IDS
max_number_of_internal_ids: 10,
# Server doesn't need to set any of this
receive_sequence: nil,
# Sequencing for whole messages
send_sequence: 0,
message_index: 0,
ordered_write_index: 0,
# Maps send_sequence indices to lists of (reliable only?) packets
unacknowledged_sent: %{},
# Sequencing for sequenced-but-not-ordered messages
sequenced_packet: 0,
packet_buffer: [],
ack_buffer: [],
mtu_size: 0,
# The :os.system_time(:millisecond) time at which we enqueued the oldest unset acknowledgement packet
oldest_unsent_ack_time_ms: 0,
# Last diff between our timestamp we sent with a ping and the time we received the pong
last_rtts: [],
# A TRef to the timer that will time out this connection and cause it to self-destruct
timeout_ref: nil,
# The instance of business_logic_module that tracks the state for this connection
client: nil
end
defmodule Resendable do
@moduledoc "A set of packets that we'll resend if we don't get an :ack"
@enforce_keys [:packets, :index, :next_resend_time]
defstruct packets: [], index: 0, next_resend_time: 0
end
@udp_header_size 28
# Sync rate is hardcoded to 1/100 sec in RakNet
@sync_ms 10
@ping_ms 5000
# This is hard-coded in RakNet, to prevent you from using all your RAM on a single client that fails to respond to
# a big stream of reliable packets
@max_tracked_reliable_packets 512
# RakNet calculated RTO dynamically based on round trip time variance and the like... we're just gonna make it 1 second + rtt
@retransmition_time_out_ms 1000
# RakNet calculates round-trip time (RTT) as the average of your up-to-5 last ping times
@rtt_window_size 5
def start_link(%State{} = state) do
GenServer.start_link(__MODULE__, state)
end
def stop(connection_pid), do: GenServer.stop(connection_pid, :shutdown)
def handle_message(connection_pid, message_type, data) do
GenServer.cast(connection_pid, {message_type, data})
end
@doc """
Returns one of:
{:ok, nil} (if you didn't request an ack receipt)
{:ok, ack_receipt_id}
{:error, message}
"""
def send(connection_pid, reliability, message)
def send(connection_pid, reliability, message)
when is_bitstring(message) and reliability in [:unreliable_ack_receipt, :reliable_ack_receipt, :reliable_ordered_ack_receipt] do
{:ok, GenServer.call(connection_pid, {:send, reliability, message})}
end
def send(connection_pid, reliability, message) when is_bitstring(message) and is_atom(reliability) do
GenServer.cast(connection_pid, {:send, reliability, message})
{:ok, nil}
end
################ Server Implementation ################
@impl GenServer
def init(state) do
{:ok, _} = :timer.send_interval(@sync_ms, :sync)
Process.flag(:trap_exit, true)
{:ok, reschedule_timeout(state)}
end
@raknet_protocol_version 6
@use_security 0
@impl GenServer
def handle_info(:sync, connection) do
# TODO: Variable retransmission timeout based on RTT: send if estimatedTimeToNextTick+curTime < oldestUnsentAck+rto-RTT
{:noreply,
connection
|> sync_ack_buffer()
|> sync_requeue_reliable_data_packets()
|> sync_enqueued_data_packets()}
end
@impl GenServer
def handle_info(:sync_ping, connection) do
{:noreply, ping(connection)}
end
@impl GenServer
def handle_info({:EXIT, _pid, reason}, connection) do
Logger.debug("Connection exiting due to reason #{inspect(reason)}")
if not is_nil(connection.client) do
RakNet.Client.disconnect(connection.client)
end
Process.exit(self(), :kill)
end
defp sync_ack_buffer(connection) do
current_time = unix_timestamp_ms()
if connection.oldest_unsent_ack_time_ms > 0 and
(connection.oldest_unsent_ack_time_ms <= current_time - @sync_ms or length(connection.ack_buffer) > 20) do
ack(sweep_line(connection.ack_buffer), connection.respond, connection)
%{connection | ack_buffer: [], oldest_unsent_ack_time_ms: 0}
else
connection
end
end
defp sync_enqueued_data_packets(connection) do
if Enum.empty?(connection.packet_buffer) do
connection
else
updated_conn = send_immediate(connection.packet_buffer, connection)
%{updated_conn | packet_buffer: []}
end
end
defp sync_requeue_reliable_data_packets(connection) do
current_time = RakNet.Server.timestamp()
# PerfTODO: This is more expensive than it needs to be... Instead use a queue of {resend_time, packet}?
to_resend =
Map.values(connection.unacknowledged_sent)
|> Enum.sort_by(select_key(:next_resend_time))
|> Enum.take_while(fn resendable -> resendable.next_resend_time <= current_time end)
if Enum.empty?(to_resend) do
connection
else
packets = to_resend |> Enum.map(select_key(:packets)) |> List.flatten()
# When the packets actually go out, we'll re-add them to unacknowledged_sent queue (with a new index)
indices = Enum.map(to_resend, select_key(:index))
%{
connection
| packet_buffer: packets ++ connection.packet_buffer,
unacknowledged_sent: Map.drop(connection.unacknowledged_sent, indices)
}
end
end
defp ping(connection) do
enqueue(:unreliable, make_ping_buffer(connection.base_time), connection)
end
defp send_immediate(packets, connection) when is_list(packets) do
encoded =
RakNet.Packet.encode(%{
sequence_number: connection.send_sequence,
encapsulated_packets: connection.packet_buffer,
timestamp: if(connection.include_timestamp_with_datagrams, do: RakNet.Server.timestamp(connection.base_time), else: -1)
})
# TODO: Periodically retransmit unacknowledged reliable packets
# TODO: Split packets larger than connection.mtu_size
# credo:disable-for-next-line
connection.respond.(<<Message.binary(:data_packet_4), encoded::binary>>, List.first(connection.client_ips_and_ports))
{reliable_packets, _} = Enum.split_with(packets, fn %ReliabilityLayer.Packet{reliability: r} -> Reliability.is_reliable?(r) end)
updated_unacknowledged =
if Enum.empty?(reliable_packets) or map_size(connection.unacknowledged_sent) > @max_tracked_reliable_packets do
connection.unacknowledged_sent
else
Map.put(connection.unacknowledged_sent, connection.send_sequence, %Resendable{
index: connection.send_sequence,
packets: reliable_packets,
next_resend_time: RakNet.Server.timestamp() + rtt(connection) * 1.5 + @retransmition_time_out_ms
})
end
%{
connection
| unacknowledged_sent: updated_unacknowledged,
ordered_write_index: 1 + max(connection.ordered_write_index, max_packet_value(reliable_packets, :order_index)),
message_index: 1 + max(connection.message_index, max_packet_value(reliable_packets, :message_index)),
send_sequence: connection.send_sequence + 1
}
end
defp max_packet_value(packets, packet_key, default \\ -1) when is_list(packets) and is_atom(packet_key) do
case Enum.max_by(packets, select_key(packet_key), fn -> default end) do
%ReliabilityLayer.Packet{} = p ->
case Map.fetch!(p, packet_key) do
num when is_number(num) -> num
_ -> default
end
_ ->
default
end
end
@impl GenServer
def handle_cast({:open_connection_request_1, data}, connection) do
Logger.debug("Received open connection request 1 from #{inspect(List.first(connection.client_ips_and_ports))}")
<<_offline_msg_id::binary-size(16), @raknet_protocol_version::size(8), _zero_pad_to_mtu_size::bitstring>> = data
# +1 for the message ID byte (:open_connection_request_1)
mtu_size = byte_size(data) + @udp_header_size + 1
message =
<<Message.binary(:open_connection_reply_1), Message.offline_msg_id()::binary, connection.server_identifier::binary,
@use_security::size(8), mtu_size::size(16)>>
# credo:disable-for-next-line
connection.respond.(message, List.first(connection.client_ips_and_ports))
Logger.debug("Sent open connection reply 1 to #{inspect(List.first(connection.client_ips_and_ports))}")
{:noreply, %{connection | mtu_size: mtu_size}}
rescue
err ->
Logger.error("Failed to parse open connection request 1 of #{byte_size(data)} bytes (needed at least 24 bytes)")
Logger.error("Packet was: #{inspect(data)}")
Logger.error("Connection state was: #{inspect(connection)}")
reraise(err, __STACKTRACE__)
end
@impl GenServer
def handle_cast({:open_connection_request_2, data}, connection) do
Logger.debug("Received open connection request 2 from #{inspect(List.first(connection.client_ips_and_ports))}")
server_addr_bytes = byte_size(data) - 16 - 2 - 8
<<_offline_id::binary-size(16), _svr_address::binary-size(server_addr_bytes), mtu_size::size(16), _client_id::size(64)>> = data
connection.respond.(
<<Message.binary(:open_connection_reply_2), Message.offline_msg_id()::binary, connection.server_identifier::binary,
connection.encoded_client::binary, mtu_size::size(16), @use_security::size(8)>>,
# credo:disable-for-next-line
List.first(connection.client_ips_and_ports)
)
Logger.debug("Sent open connection reply 2 to #{inspect(List.first(connection.client_ips_and_ports))}")
{:noreply, connection}
end
@impl GenServer
def handle_cast({:ping, <<ping_time::size(64)>>}, connection) do
{:noreply, enqueue(:unreliable, make_pong_buffer(ping_time, connection.base_time), reschedule_timeout(connection))}
end
@impl GenServer
def handle_cast({:pong, <<our_sent_time::size(64), _::binary>>}, %State{last_rtts: prev_rtts} = connection) do
ping_time = RakNet.Server.timestamp(connection.base_time) - our_sent_time
updated_rtts = [ping_time | Enum.take(prev_rtts, @rtt_window_size - 1)]
{:noreply, reschedule_timeout(%{connection | last_rtts: updated_rtts})}
end
@impl GenServer
def handle_cast({:ack, packet}, %State{unacknowledged_sent: unacked} = connection) do
timestamp_size = if connection.include_timestamp_with_datagrams, do: 32, else: 0
# TODO: Do something with the timestamp if > 0
<<_timestamp::size(timestamp_size), ack_portion::binary>> = packet
{removed, still_unacked} = Map.split(unacked, message_indices_from_ack(ack_portion))
msgs_received =
removed
|> Map.values()
# Values from the removed map are Resendable structs; we just need to inspect the packets
|> Enum.flat_map(select_key(:packets))
|> Enum.filter(fn %ReliabilityLayer.Packet{reliability: r} -> Reliability.needs_client_ack?(r) end)
|> Enum.map(select_key(:message_index))
|> MapSet.new()
Enum.each(msgs_received, fn msg_idx -> RakNet.Client.got_ack(connection.client, msg_idx) end)
{:noreply, %{connection | unacknowledged_sent: still_unacked}}
end
@impl GenServer
def handle_cast({:nack, _packet}, connection) do
# TODO: Resend?
{:noreply, connection}
end
@impl GenServer
def handle_cast({:client_connect, data}, connection) do
Logger.debug("Received client connect from #{inspect(List.first(connection.client_ips_and_ports))}")
<<_client_id::size(64), time_sent::size(64), @use_security::size(8), _password::binary>> = data
send_pong = RakNet.Server.timestamp(connection.base_time)
# TODO: Support IPv6
# TODO: Should we offer other ports clients can connect on?
empty_ip =
RakNet.SystemAddress.encode(%{
version: 4,
address: {255, 255, 255, 255},
port: 0
})
packet =
<<Message.binary(:server_handshake), connection.encoded_client::binary, 0::size(16)>> <>
:erlang.list_to_binary([connection.encoded_host] ++ List.duplicate(empty_ip, 9)) <>
<<time_sent::size(64), send_pong::size(64)>>
Logger.debug("Sent server handshake to #{inspect(List.first(connection.client_ips_and_ports))}")
{:noreply, enqueue(:reliable_ordered, packet, connection)}
end
@impl GenServer
def handle_cast({:client_handshake, data}, connection) do
Logger.debug("Received client handshake from #{inspect(List.first(connection.client_ips_and_ports))}")
# A system address is: 1 byte v4 or v6, followed by EITHER 4 bytes v4 address + 2 bytes port OR 28 bytes v6 addr & port
addresses_length = bit_size(data) - 2 * 64
<<_addresses::bitstring-size(addresses_length), ping_time::size(64), pong_time::size(64)>> = data
# We send the first ping immediately, then subsequent pings every 5 seconds
updated_conn =
enqueue(
:unreliable,
[
make_ping_buffer(connection.base_time),
make_pong_buffer(ping_time, connection.base_time)
],
connection
)
{:ok, _} = :timer.send_interval(@ping_ms, :sync_ping)
rtt = max(0, pong_time - RakNet.Server.timestamp(connection.base_time))
client = RakNet.Client.new(connection.client_module, self(), connection.client_data)
Logger.debug("Finalized connection handshake with #{inspect(List.first(connection.client_ips_and_ports))}")
{:noreply, %{updated_conn | last_rtts: [rtt], client: client}}
end
@impl GenServer
def handle_cast({:client_disconnect, _data}, connection) do
Logger.debug("Client #{inspect(List.first(connection.client_ips_and_ports))} disconnected")
if not is_nil(connection.client) do
RakNet.Client.disconnect(connection.client)
end
Process.exit(self(), :normal)
{:noreply, connection}
end
@impl GenServer
def handle_cast({packet_type, data}, %State{} = connection) when is_atom(packet_type) do
%{encapsulated_packets: encapsulated_packets, sequence_number: recv_sequence, timestamp: _timestamp} =
if connection.include_timestamp_with_datagrams do
RakNet.Packet.decode_with_timestamp(data)
else
RakNet.Packet.decode_no_timestamp(data)
end
if Enum.empty?(encapsulated_packets) do
# Had a parsing error!
Logger.error("Failed to parse #{packet_type} (#{byte_size(data) + 1} bytes) #{inspect(data, limit: :infinity)}")
{:noreply, connection}
else
{:noreply,
encapsulated_packets
|> Enum.reduce({connection, :unacknowledged}, fn packet, {conn, acked} ->
<<identifier::size(8), head_data::binary>> = packet.buffer
ident_atom = Message.name(identifier)
is_connection_negotiation = ident_atom in [:ping, :pong, :client_connect, :client_handshake, :client_disconnect]
finished_connection_negotiation = not Enum.empty?(conn.last_rtts) and conn.client != nil
if is_connection_negotiation or finished_connection_negotiation do
# TODO: Sequence indices are per-channel
# TODO: Immediately send acks for split packets
updated_conn =
if acked == :unacknowledged do
%{buffer_ack(recv_sequence, conn) | receive_sequence: max(conn.receive_sequence, recv_sequence)}
else
conn
end
if is_connection_negotiation do
{elem(handle_cast({ident_atom, head_data}, updated_conn), 1), :acknowledged}
else
# TODO: Support custom packet type atoms (right now we're passing through the integer values as the identifier)
RakNet.Client.receive(updated_conn.client, identifier, head_data, rtt(updated_conn) / 2)
{reschedule_timeout(updated_conn), :acknowledged}
end
else
Logger.info("Connection to #{inspect(conn.host)}:#{conn.port} received data before connection negotiation finished")
# credo:disable-for-next-line
conn.respond.(<<RakNet.Message.binary(:connection_lost)>>, List.first(conn.client_ips_and_ports))
{conn, acked}
end
end)
|> elem(0)}
end
end
@impl GenServer
def handle_cast({:send, reliability, message}, %State{} = connection) do
{:noreply, sync_enqueued_data_packets(enqueue(reliability, message, connection))}
end
@impl GenServer
def handle_call({:send, reliability, message}, _from, %State{} = connection) do
updated_conn = sync_enqueued_data_packets(enqueue(reliability, message, connection))
{:reply, updated_conn.message_index, updated_conn}
end
@impl GenServer
def terminate(reason, connection) do
Logger.info("Terminating #{inspect(connection.host)}:#{connection.port} due to #{inspect(reason)}")
Registry.unregister(RakNet.Connection, {connection.host, connection.port})
end
defp reschedule_timeout(%State{timeout_ref: nil} = connection) do
case :timer.exit_after(connection.timeout_ms, self(), :timeout) do
{:ok, timer_id} -> %{connection | timeout_ref: timer_id}
_ -> connection
end
end
defp reschedule_timeout(%State{} = connection) do
case :timer.cancel(connection.timeout_ref) do
{:ok, _} -> reschedule_timeout(%{connection | timeout_ref: nil})
# I guess we try killing it again later?
_ -> connection
end
end
defp enqueue(reliability, buffer, connection) when is_atom(reliability) and is_bitstring(buffer) do
enqueue(reliability, [buffer], connection)
end
defp enqueue(reliability, buffers, connection)
when (reliability == :unreliable_sequenced or reliability == :reliable_sequenced or reliability == :reliable_ordered_ack_receipt) and
is_list(buffers) do
num_buffers = length(buffers)
new_packets =
buffers
|> Enum.zip(0..(num_buffers - 1))
|> Enum.map(fn {buffer, idx} ->
%ReliabilityLayer.Packet{
reliability: reliability,
message_index: if(Reliability.is_reliable?(reliability), do: connection.message_index, else: nil),
order_index: connection.ordered_write_index,
sequencing_index: connection.sequenced_packet + idx,
buffer: buffer
}
end)
%{connection | packet_buffer: new_packets ++ connection.packet_buffer, sequenced_packet: connection.sequenced_packet + num_buffers}
end
defp enqueue(reliability, buffers, connection) when is_atom(reliability) and is_list(buffers) do
if Reliability.valid?(reliability) do
new_packets =
Enum.map(buffers, fn buffer ->
%ReliabilityLayer.Packet{
reliability: reliability,
message_index:
if(Reliability.is_reliable?(reliability) or Reliability.needs_client_ack?(reliability),
do: connection.message_index,
else: nil
),
order_index: if(Reliability.is_sequenced?(reliability), do: connection.ordered_write_index, else: nil),
buffer: buffer
}
end)
%{connection | packet_buffer: new_packets ++ connection.packet_buffer}
else
Logger.error("Unknown reliability atom #{reliability}")
connection
end
end
defp buffer_ack(packet_index, connection) when is_integer(packet_index) do
case connection.ack_buffer do
[] -> %{connection | ack_buffer: [packet_index], oldest_unsent_ack_time_ms: unix_timestamp_ms()}
_ -> %{connection | ack_buffer: [packet_index | connection.ack_buffer]}
end
end
@doc """
The sweep line algorithm: https://en.wikipedia.org/wiki/Sweep_line_algorithm
Given a collection of integers, it combines them into the minimum number of contiguous ranges.
iex> RakNet.Connection.sweep_line(MapSet.new([5, 4, 3, 2, 1, 0]))
[{0, 5}]
iex> RakNet.Connection.sweep_line([5, 4, 3, 100, 1, 0])
[{0, 1}, {3, 5}, {100, 100}]
"""
def sweep_line(integers) do
[start | sorted] = Enum.sort(integers)
# Traverse the sorted array, while maintaining a set of "active" events:
# whenever you see a start/end time, respectively add or drop the
# corresponding event from the set, and add (if the active set is non-empty)
# an event to your solution.
sorted
|> Enum.reduce([{start, start}], fn packet_idx, [{b, e} | rest] = acc ->
if packet_idx == e + 1 do
[{b, packet_idx} | rest]
else
[{packet_idx, packet_idx} | acc]
end
end)
|> Enum.sort()
end
defp ack(buffered_acks, responder, connection) when is_list(buffered_acks) do
{timestamp_bits, timestamp} =
if connection.include_timestamp_with_datagrams do
{32, RakNet.Server.timestamp(connection.base_time)}
else
{0, 0}
end
responder.(
<<Message.binary(:ack), timestamp::size(timestamp_bits), length(buffered_acks)::size(16)>> <>
:erlang.list_to_binary(
Enum.map(buffered_acks, fn {range_min, range_max} ->
min_is_max = if range_min == range_max, do: 1, else: 0
<<min_is_max::size(8), range_min::little-size(24)>> <>
if(min_is_max == 1, do: <<>>, else: <<range_max::little-size(24)>>)
end)
),
# credo:disable-for-next-line
List.first(connection.client_ips_and_ports)
)
end
def message_indices_from_ack(<<packet_count::size(16), remainder::binary>> = full) do
parsed =
Enum.reduce(1..packet_count, {[], remainder}, fn _, {indices_to_drop, rem_binary} ->
case rem_binary do
# 1 -> range min == range max (i.e., it's a single index)
<<1::size(8), rmin::little-size(24), rem::binary>> -> {[rmin | indices_to_drop], rem}
<<0::size(8), rmin::little-size(24), rmax::little-size(24)>> -> {Enum.to_list(rmin..rmax) ++ indices_to_drop, <<>>}
<<0::size(8), rmin::little-size(24), rmax::little-size(24), rem::binary>> -> {Enum.to_list(rmin..rmax) ++ indices_to_drop, rem}
_ -> :error
end
end)
case parsed do
{msg_indices_to_drop, <<>>} ->
msg_indices_to_drop
_ ->
Logger.error("Ack packet failed to parse: #{inspect(full)}")
[]
end
end
defp make_ping_buffer(base_time) do
<<Message.binary(:ping), RakNet.Server.timestamp(base_time)::size(64)>>
end
defp make_pong_buffer(ping_time, base_time) do
<<Message.binary(:pong), ping_time::size(64), RakNet.Server.timestamp(base_time)::size(64)>>
end
# Assume a high-but-not-crazy ping until the connection negotiation is finished (for the purpose of scheduling retries)
defp rtt(%State{last_rtts: []}), do: 200
defp rtt(%State{last_rtts: rtts}), do: Enum.sum(rtts) / length(rtts)
end
================================================
FILE: lib/message.ex
================================================
defmodule RakNet.Message do
@moduledoc """
Message types that RakNet can send. These are the first 8 bits of the packet.
Follows RakLib protocol: https://github.com/pmmp/RakLib/tree/master/src/protocol
"""
@names_and_vals %{
# These come from RakNet's DefaultMessageIDTypes enum (in MessageIdentifiers.h)
:ping => 0x00,
:unconnected_ping => 0x01,
:unconnected_ping_open_connections => 0x02,
:pong => 0x03,
:open_connection_request_1 => 0x05,
:open_connection_reply_1 => 0x06,
:open_connection_request_2 => 0x07,
:open_connection_reply_2 => 0x08,
# ID_CONNECTION_REQUEST
:client_connect => 0x09,
# ID_CONNECTION_REQUEST_ACCEPTED: Tell the client the connection request accepted
:server_handshake => 0x10,
# ID_CONNECTION_ATTEMPT_FAILED: Sent to the player when a connection request cannot be completed
:connection_attempt_failed => 0x11,
# ID_NEW_INCOMING_CONNECTION: A remote client has successfully connected
:client_handshake => 0x13,
:client_disconnect => 0x15,
# ID_CONNECTION_LOST: Reliable packet delivery failed, connection closed
:connection_lost => 0x16,
# ID_INCOMPATIBLE_PROTOCOL_VERSION
:incompatible_version => 0x19,
:unconnected_pong => 0x1C,
:advertise_system => 0x1D,
# These come from RakNet's reliability layer and "congestion control"---they are datagram headers
# The indices are the priority (0 to 15)
:data_packet_0 => 0x80,
:data_packet_1 => 0x81,
:data_packet_2 => 0x82,
:data_packet_3 => 0x83,
:data_packet_4 => 0x84,
:data_packet_5 => 0x85,
:data_packet_6 => 0x86,
:data_packet_7 => 0x87,
:data_packet_8 => 0x88,
:data_packet_9 => 0x89,
:data_packet_A => 0x8A,
:data_packet_B => 0x8B,
:data_packet_C => 0x8C,
:data_packet_D => 0x8D,
:data_packet_E => 0x8E,
:data_packet_F => 0x8F,
:nack => 0xA0,
:ack => 0xC0
}
@msg_names MapSet.new(Map.keys(@names_and_vals))
@msg_binary_vals MapSet.new(Map.values(@names_and_vals))
@vals_and_names Map.new(@names_and_vals, fn {name, val} -> {val, name} end)
@doc """
Set of all message types listed above---:ping, :unconnected_ping, :pong, etc.
"""
def known_message_names, do: @msg_names
@doc """
Set of all hex values that act as packet type identifiers.
E.g., 0x0 for ping, 0x3 for pong, 0x1d for advertise system, etc.
"""
def known_messages, do: @msg_binary_vals
@doc """
True if we have a name atom for this message type
"""
def is_known?(message_binary) when is_integer(message_binary), do: MapSet.member?(@msg_binary_vals, message_binary)
def is_protocol_data?(message_binary) when is_integer(message_binary), do: message_binary in 0x80..0x8F
@doc """
The message name atom for this binary message; :error if we don't recognize it
"""
def name(message_binary) when is_integer(message_binary), do: Map.get(@vals_and_names, message_binary, :error)
def binary(message_name) when is_atom(message_name), do: Map.fetch!(@names_and_vals, message_name)
# "Magic" bytes used to distinguish offline messages from garbage
def offline_msg_id, do: <<0, 255, 255, 0, 254, 254, 254, 254, 253, 253, 253, 253, 18, 52, 86, 120>>
end
================================================
FILE: lib/packet.ex
================================================
defmodule RakNet.Packet do
@moduledoc "Encoding & decoding utils for RakLib data, taken from ExRakLib"
require Logger
alias RakNet.ReliabilityLayer
alias RakNet.ReliabilityLayer.Reliability
def decode_with_timestamp(<<timestamp::size(32), data::binary>>) do
Map.put(decode_no_timestamp(data), :timestamp, timestamp)
end
def decode_no_timestamp(data, internal \\ false) do
<<sequence_number::little-size(24), rest::binary>> = :erlang.iolist_to_binary(data)
%{
sequence_number: sequence_number,
encapsulated_packets: decode_no_timestamp(rest, [], internal),
timestamp: -1
}
rescue
e ->
Logger.error(inspect(e))
%{sequence_number: -1, encapsulated_packets: []}
end
defp decode_no_timestamp("", encapsulated_packets, _internal) do
Enum.reverse(encapsulated_packets)
end
defp decode_no_timestamp(rest, encapsulated_packets, internal) do
{packet, rest} = decode_encapsulated_packet(rest, internal)
decode_no_timestamp(rest, [packet | encapsulated_packets], internal)
end
def encode(%{sequence_number: seq, encapsulated_packets: packets, timestamp: ts}) when ts >= 0 do
:erlang.iolist_to_binary([<<ts::size(32), seq::little-size(24)>>, Enum.map(packets, &encode_encapsulated_packet(&1, false))])
end
def encode(%{sequence_number: seq_number, encapsulated_packets: encapsulated}, internal \\ false) do
:erlang.iolist_to_binary([<<seq_number::little-size(24)>>, Enum.map(encapsulated, &encode_encapsulated_packet(&1, internal))])
end
def decode_encapsulated_packet(data, internal) do
<<reliability::unsigned-size(3), has_split::unsigned-size(5), post_header::binary>> = data
is_reliable = Reliability.is_reliable?(reliability)
is_sequenced = Reliability.is_sequenced?(reliability)
{length, identifier_ack, post_length} =
if internal do
<<length::size(32), identifier_ack::size(32), rest::binary>> = post_header
{length, identifier_ack, rest}
else
<<length::size(16), rest::binary>> = post_header
{trunc(Float.ceil(length / 8)), nil, rest}
end
# message_index is actually the sequencing index for :unreliable_sequenced
{message_index, post_reliability} =
if is_reliable or is_sequenced do
<<message_index::little-size(24), rest::binary>> = post_length
{message_index, rest}
else
{nil, post_length}
end
{order_index, order_channel, post_ordering} =
if is_sequenced do
<<order_index::little-size(24), order_channel::size(8), rest::binary>> = post_reliability
{order_index, order_channel, rest}
else
{nil, nil, post_reliability}
end
{split_count, split_id, split_index, post_split} =
if has_split > 0 do
<<split_count::size(32), split_id::size(16), split_index::size(32), rest::binary>> = post_ordering
{split_count, split_id, split_index, rest}
else
{nil, nil, nil, post_ordering}
end
<<buffer::binary-size(length), rest::binary>> = post_split
{%ReliabilityLayer.Packet{
reliability: Reliability.name(reliability),
has_split: has_split,
length: length,
identifier_ack: identifier_ack,
message_index: if(is_reliable, do: message_index, else: nil),
sequencing_index: if(is_reliable, do: nil, else: message_index),
order_index: order_index,
order_channel: order_channel,
split_count: split_count,
split_id: split_id,
split_index: split_index,
buffer: buffer
}, rest}
end
def encode_encapsulated_packet(%ReliabilityLayer.Packet{} = p, internal) do
if ReliabilityLayer.Packet.valid?(p) do
is_reliable = Reliability.is_reliable?(p.reliability)
is_sequenced = Reliability.is_sequenced?(p.reliability)
index = if is_reliable, do: p.message_index, else: p.sequencing_index
# TODO: Support splitting: https://github.com/mhsjlw/node-raknet/blob/master/src/client.js#L144
<<Reliability.binary(p.reliability)::unsigned-size(3), p.has_split::unsigned-size(5)>> <>
if internal do
<<byte_size(p.buffer)::size(32), p.identifier_ack::size(32)>>
else
<<trunc(byte_size(p.buffer) * 8)::size(16)>>
end <>
if is_reliable or is_sequenced do
<<index::little-size(24)>> <>
if is_sequenced do
<<p.order_index::little-size(24), p.order_channel::size(8)>>
else
<<>>
end
else
<<>>
end <>
if p.has_split > 0 do
<<p.split_count::size(32), p.split_id::size(16), p.split_index::size(32)>>
else
<<>>
end <>
p.buffer
else
Logger.error("Invalid packet: #{inspect(p, binaries: :as_binaries, limit: :infinity)}")
<<>>
end
end
end
================================================
FILE: lib/reliability_layer.ex
================================================
defmodule RakNet.ReliabilityLayer.Reliability do
@moduledoc "Taken from RakNet's PacketPriority.h"
@names_and_vals %{
:unreliable => 0,
:unreliable_sequenced => 1,
:reliable => 2,
:reliable_ordered => 3,
:reliable_sequenced => 4,
# These are the same as unreliable/reliable/reliable ordered, except that the business logic provider
# will get an :ack message when the client acknowledges receipt
:unreliable_ack_receipt => 5,
:reliable_ack_receipt => 6,
:reliable_ordered_ack_receipt => 7
}
@vals_and_names Map.new(@names_and_vals, fn {name, val} -> {val, name} end)
@doc """
The message name atom for this binary message; :error if we don't recognize it
"""
def name(reliability_binary) when is_integer(reliability_binary), do: Map.get(@vals_and_names, reliability_binary, :error)
def binary(reliability_name) when is_atom(reliability_name), do: Map.fetch!(@names_and_vals, reliability_name)
def valid?(reliability_atom) when is_atom(reliability_atom), do: Map.has_key?(@names_and_vals, reliability_atom)
def is_reliable?(reliability_binary) when is_integer(reliability_binary), do: reliability_binary in [2, 3, 4, 6, 7]
def is_reliable?(reliability_atom) when is_atom(reliability_atom), do: binary(reliability_atom) in [2, 3, 4, 6, 7]
def is_ordered?(reliability_binary) when is_integer(reliability_binary), do: reliability_binary == 3
def is_ordered?(reliability_atom) when is_atom(reliability_atom), do: reliability_atom == :reliable_ordered
def is_sequenced?(reliability_binary) when is_integer(reliability_binary), do: reliability_binary in [1, 3, 4, 7]
def is_sequenced?(reliability_atom) when is_atom(reliability_atom), do: binary(reliability_atom) in [1, 3, 4, 7]
def needs_client_ack?(reliability_binary) when is_integer(reliability_binary), do: reliability_binary in [5, 6, 7]
def needs_client_ack?(reliability_atom) when is_atom(reliability_atom), do: binary(reliability_atom) in [5, 6, 7]
end
defmodule RakNet.ReliabilityLayer.Packet do
@moduledoc "See ReliabilityLayer.cpp, ReliabilityLayer::WriteToBitStreamFromInternalPacket()"
alias RakNet.ReliabilityLayer.Reliability
@enforce_keys [:reliability, :buffer]
defstruct priority: 4,
reliability: Reliability.binary(:reliable_ordered),
has_split: 0,
length: -1,
# Used for internal packets only
identifier_ack: nil,
# Used for all reliable types
message_index: nil,
# Used for UNRELIABLE_SEQUENCED, RELIABLE_SEQUENCED
sequencing_index: nil,
# Used for UNRELIABLE_SEQUENCED, RELIABLE_SEQUENCED, RELIABLE_ORDERED.
order_index: nil,
order_channel: 0,
# Split packets only
split_count: nil,
split_id: nil,
split_index: nil,
# The actual (encapsulated) packet data
buffer: nil
# credo:disable-for-next-line
def valid?(%RakNet.ReliabilityLayer.Packet{} = p) do
msg_idx_ok = not Reliability.is_reliable?(p.reliability) or (is_integer(p.message_index) and p.message_index >= 0)
order_idx_ok = not Reliability.is_sequenced?(p.reliability) or (is_integer(p.order_index) and p.order_index >= 0)
split_ok =
p.has_split == 0 or
(is_integer(p.split_count) and p.split_count > 0 and
is_integer(p.split_id) and is_integer(p.split_index))
msg_idx_ok and order_idx_ok and split_ok and
p.priority >= 0 and p.priority < 0xF and
Reliability.valid?(p.reliability) and p.buffer != nil
end
end
================================================
FILE: lib/server.ex
================================================
defmodule RakNet.Server do
@moduledoc """
A server implementing the RakNet protocol
"""
require Logger
import XUtil.Time, only: [unix_timestamp_ms: 0]
defmodule Spawner do
@moduledoc "A dumb wrapper to make a spawned function supervisable"
def start_link(impl_fn), do: {:ok, spawn(impl_fn)}
end
def child_spec([receiver | [port | options]]) do
%{
# String.to_atom/1 is fine here, because we'll be creating a very limited number of listeners per port
# credo:disable-for-next-line
id: String.to_atom("#{__MODULE__}:#{port}"),
start: {__MODULE__, :start_link, [receiver, port, options]},
restart: :permanent,
type: :worker
}
end
def start_link(client_module, port, options \\ []) when is_atom(client_module) and is_integer(port) do
defaults = [
custom_packets: [],
custom_types: [],
client_data: %{},
client_timeout_ms: 10_000,
# Corresponds to #define INCLUDE_TIMESTAMP_WITH_DATAGRAMS
# (which is in turn enabled by USE_SLIDING_WINDOW_CONGESTION_CONTROL in the RakNetDefinesOverrides.h)
include_timestamp_with_datagrams: false,
# Corresponds to #define MAXIMUM_NUMBER_OF_INTERNAL_IDS
max_number_of_internal_ids: 10,
open_ipv4_socket: true,
open_ipv6_socket: System.get_env("SEPARATE_IPV6_PORT") != "false",
# TODO: use gen_udp directly and reopen the socket if it gets closed
send: &Socket.Datagram.send!/3,
server_identifier: make_unique_id(),
offline_ping_response: "RakNet Server"
]
host = Keyword.get(options, :host, {0, 0, 0, 0})
config =
defaults
|> Keyword.merge(options)
|> Enum.into(%{
client_module: struct(client_module),
encoded_host: RakNet.SystemAddress.encode(%{address: host, port: port})
})
socket_v4 = if config[:open_ipv4_socket], do: elem(open_socket(4, port), 1), else: nil
socket_v6 = if config[:open_ipv6_socket], do: elem(open_socket(6, port), 1), else: nil
sockets = {socket_v4, socket_v6}
Supervisor.start_link(
[
# String.to_atom/1 is fine here, because we'll be creating a very limited number of listeners per port
# credo:disable-for-lines:2
%{id: String.to_atom("#{__MODULE__}:#{port}_ipv4"), start: {Spawner, :start_link, [fn -> serve_v4(sockets, config) end]}},
%{id: String.to_atom("#{__MODULE__}:#{port}_ipv6"), start: {Spawner, :start_link, [fn -> serve_v6(sockets, config) end]}}
],
strategy: :one_for_one
)
end
defp open_socket(ip_version, port) when ip_version == 4 or ip_version == 6 do
addl_args = if ip_version == 4, do: [:inet], else: [:inet6, {:ipv6_v6only, true}]
:gen_udp.open(port, [:binary, {:active, false}] ++ addl_args)
end
@doc "The current Unix timestamp, in milliseconds"
def timestamp(offset \\ 0), do: unix_timestamp_ms() - offset
@doc "A 64-bit unique ID"
def make_unique_id, do: <<timestamp()::size(48), :rand.uniform(65_536)::size(16)>>
defp serve_v4({nil, _}, _config), do: :ok
defp serve_v4({socket_v4, _} = sockets, config), do: serve_impl(&serve_v4/2, socket_v4, sockets, config)
defp serve_v6({_, nil}, _config), do: :ok
defp serve_v6({_, socket_v6} = sockets, config), do: serve_impl(&serve_v6/2, socket_v6, sockets, config)
defp serve_impl(server, receive_socket, sockets, config) do
# We have to do basic decoding in the main process, because it may update our client connection state. :(
case Socket.Datagram.recv!(receive_socket) do
nil ->
server.(sockets, config)
{packet, {client_ip, client_port}} = received_raw ->
case decode(received_raw, sockets, config) do
# Send to a client process to parse & optionally respond (never do any work here, since this is a single process)
{:connected, {client, packet_type, data}} -> RakNet.Connection.handle_message(client, packet_type, data)
{:unconnected, decoded} -> spawn(fn -> handle_unconnected_packet(sockets, config, decoded) end)
_ -> Logger.error("Failed to decode packet from #{inspect(client_ip)}:#{client_port}\nPacket: #{inspect(packet)}")
end
end
server.(sockets, config)
end
# One of:
# {:error, %{}}
# {:unconnected, {client_ip_and_port, :unconnected_ping, data}}
# {:connected, {client, packet_type, data}}
defp decode({packet, client_ip_and_port}, sockets, config) do
case packet_decode(packet, config) do
{:error, _msg} = err ->
err
{:ok, :open_connection_request_1, data} ->
{:connected, {open_connection(sockets, client_ip_and_port, config), :open_connection_request_1, data}}
{:ok, :unconnected_ping, data} ->
{:unconnected, {client_ip_and_port, :unconnected_ping, data}}
{:ok, :unconnected_ping_open_connections, data} ->
{:unconnected, {client_ip_and_port, :unconnected_ping_open_connections, data}}
{:ok, packet_type, data} ->
case lookup(client_ip_and_port) do
nil -> {:unconnected, {client_ip_and_port, packet_type, data}}
client -> {:connected, {client, packet_type, data}}
end
end
end
defp open_connection(sockets, {host, port} = client_ip_and_port, config) do
existing_client = lookup(client_ip_and_port)
# TODO: Nuke the existing client, create a new one
if existing_client do
Logger.debug("Existing client requested to open a new connection")
RakNet.Connection.stop(existing_client)
else
Logger.debug("Open a new connection to #{inspect(host)}:#{port}")
end
{:ok, new_client} =
RakNet.Connection.start_link(%RakNet.Connection.State{
host: host,
port: port,
encoded_host: config[:encoded_host],
client_ips_and_ports: [client_ip_and_port],
encoded_client: RakNet.SystemAddress.encode(%{address: host, port: port}),
timeout_ms: config[:client_timeout_ms],
base_time: timestamp(),
server_identifier: config[:server_identifier],
client_module: config[:client_module],
client_data: config[:client_data],
include_timestamp_with_datagrams: config[:include_timestamp_with_datagrams],
max_number_of_internal_ids: config[:max_number_of_internal_ids],
respond: make_responder(sockets, config)
})
# Do *not* bring down the whole server if the connection dies; if that happens,
# we'll just start a new proceses next time we hear from the user.
Process.unlink(new_client)
Registry.register(RakNet.Connection, client_ip_and_port, new_client)
new_client
end
defp lookup(client_ip_and_port) do
case Registry.lookup(RakNet.Connection, client_ip_and_port) do
[{_, client}] ->
if Process.alive?(client) do
client
else
Registry.unregister(RakNet.Connection, client_ip_and_port)
nil
end
_ ->
nil
end
end
defp packet_decode(<<identifier::unsigned-size(8), data::binary>>, _config) do
case RakNet.Message.name(identifier) do
:error -> {:error, "Unknown packet identifier"}
name -> {:ok, name, data}
end
end
defp handle_unconnected_packet(sockets, config, {client_ip_port, :unconnected_ping, <<ping_time::size(64), _::binary>>}) do
send_unconnected_pong(sockets, client_ip_port, ping_time, config)
end
defp handle_unconnected_packet(sockets, config, {client_ip_port, :unconnected_ping_open_connections, <<ping_time::size(64), _::binary>>}) do
send_unconnected_pong(sockets, client_ip_port, ping_time, config)
end
defp handle_unconnected_packet(sockets, config, {client_ip_port, _other, _data}) do
send(sockets, <<RakNet.Message.binary(:connection_lost)>>, client_ip_port, config)
end
defp send_unconnected_pong(sockets, client_ip_port, ping_time, config) do
send(
sockets,
<<RakNet.Message.binary(:unconnected_pong), ping_time::size(64), config[:server_identifier]::binary,
RakNet.Message.offline_msg_id()::binary>>,
client_ip_port,
config
)
end
defp make_responder(sockets, config) do
fn packet, ip_and_port ->
send(sockets, packet, ip_and_port, config)
end
end
defp send(sockets, packet, client_ip_and_port, config) do
sockets
|> choose_socket(client_ip_and_port)
|> config[:send].(packet, client_ip_and_port)
end
defp choose_socket({socket_v4, socket_v6}, client_ip_and_port) do
case RakNet.SystemAddress.ip_version(client_ip_and_port) do
4 -> socket_v4
6 -> socket_v6
end
end
end
================================================
FILE: lib/system_address.ex
================================================
defmodule RakNet.SystemAddress do
@moduledoc "Tools for encoding & decoding IP addresses & ports"
def encode(%{version: 4, address: {o1, o2, o3, o4}, port: port}),
do: <<4::size(8), o1::unsigned-size(8), o2::unsigned-size(8), o3::unsigned-size(8), o4::unsigned-size(8), port::unsigned-size(16)>>
def encode(%{version: 6, address: {h1, h2, h3, h4, h5, h6, h7, h8}, port: port}) do
<<6::size(8), 28::unsigned-size(8), 30::unsigned-size(8), port::unsigned-size(16), 0::size(32), h1::unsigned-size(16),
h2::unsigned-size(16), h3::unsigned-size(16), h4::unsigned-size(16), h5::unsigned-size(16), h6::unsigned-size(16),
h7::unsigned-size(16), h8::unsigned-size(16), 0::size(32)>>
end
def encode(%{address: addr, port: _port} = args) do
encode(Map.put(args, :version, ip_version(addr)))
end
# Octet and hextet versions
def ip_version({_, _, _, _}), do: 4
def ip_version({_, _, _, _, _, _, _, _}), do: 6
# Host-and-port patterns
def ip_version({{_, _, _, _}, port}) when is_integer(port), do: 4
def ip_version({{_, _, _, _, _, _, _, _}, port}) when is_integer(port), do: 6
def decode_many(addresses_and_ports) when is_bitstring(addresses_and_ports) do
decode_address_port(addresses_and_ports)
end
defp decode_address_port(bin, prev \\ [])
defp decode_address_port(<<4::size(8), address::binary-size(4), port::unsigned-size(16), rest::binary>>, prev) do
<<o1::unsigned-size(8), o2::unsigned-size(8), o3::unsigned-size(8), o4::unsigned-size(8)>> = address
decode_address_port(rest, [%{version: 4, address: {o1, o2, o3, o4}, port: port} | prev])
end
# Note: ipv6 addresses are serialized as their whole sockaddr_in6 struct
# Fields are:
# 1. Length of the struct (28 bytes == 224 bits)
# 2. sa_family_t, fixed value of AF_INET6 == 30 (0x1e)
# 3. port
# 4. flow info (???)
# 5. in6_addr (128 bits)
# 6. scope ID (???)
defp decode_address_port(<<6::size(8), 28::unsigned-size(8), 30::unsigned-size(8), ipv6_body::bitstring-size(208), rest::binary>>, prev) do
<<port::unsigned-size(16), _::unsigned-size(32), addr::bitstring-size(128), _::unsigned-size(32)>> = ipv6_body
<<a1::unsigned-size(16), a2::unsigned-size(16), a3::unsigned-size(16), a4::unsigned-size(16), a5::unsigned-size(16),
a6::unsigned-size(16), a7::unsigned-size(16), a8::unsigned-size(16)>> = addr
decode_address_port(rest, [%{version: 6, address: {a1, a2, a3, a4, a5, a6, a7, a8}, port: port} | prev])
end
defp decode_address_port(<<>>, prev) when is_list(prev), do: prev
end
================================================
FILE: mix.exs
================================================
defmodule RakNet.MixProject do
use Mix.Project
def project do
[
app: :raknet,
version: "0.1.0",
build_path: "_build",
deps_path: "deps",
lockfile: "mix.lock",
elixir: "~> 1.11",
elixirc_options: [warnings_as_errors: halt_on_warnings?(Mix.env())],
start_permanent: Mix.env() == :prod,
consolidate_protocols: Mix.env() != :test,
deps: deps()
]
end
def application do
[
extra_applications: [:logger],
mod: {RakNet.Application, []}
]
end
defp deps do
[
{:x_util, git: "https://github.com/X-Plane/elixir-xutil.git", branch: "main"},
{:socket, "~> 0.3.13"},
{:assertions, "~> 0.10", only: :test},
{:credo, "~> 1.5.1", only: [:dev, :test], runtime: false}
]
end
# Clever hack to allow unused functions and the like in test, but not dev or prod:
# https://blog.rentpathcode.com/elixir-warnings-as-errors-sometimes-f5a8d2c96b15
defp halt_on_warnings?(:test), do: false
defp halt_on_warnings?(_), do: true
end
================================================
FILE: raknet.iml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<module type="ELIXIR_MODULE" version="4">
<component name="NewModuleRootManager">
<output url="file://$MODULE_DIR$/_build/dev/lib/raknet/ebin" />
<output-test url="file://$MODULE_DIR$/_build/test/lib/raknet/ebin" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/lib" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/assets/node_modules/phoenix" />
<excludeFolder url="file://$MODULE_DIR$/assets/node_modules/phoenix_html" />
<excludeFolder url="file://$MODULE_DIR$/cover" />
<excludeFolder url="file://$MODULE_DIR$/deps" />
<excludeFolder url="file://$MODULE_DIR$/doc" />
<excludeFolder url="file://$MODULE_DIR$/logs" />
<excludeFolder url="file://$MODULE_DIR$/_build/test/lib/comb" />
<excludeFolder url="file://$MODULE_DIR$/_build/test/lib/socket" />
<excludeFolder url="file://$MODULE_DIR$/_build/test/lib/x_util" />
<excludeFolder url="file://$MODULE_DIR$/_build/test/lib/assertions" />
<excludeFolder url="file://$MODULE_DIR$/_build/dev/lib/socket" />
<excludeFolder url="file://$MODULE_DIR$/_build/dev/lib/comb" />
<excludeFolder url="file://$MODULE_DIR$/_build/dev/lib/x_util" />
<excludeFolder url="file://$MODULE_DIR$/_build/dev/lib/bunt" />
<excludeFolder url="file://$MODULE_DIR$/_build/dev/lib/credo" />
<excludeFolder url="file://$MODULE_DIR$/_build/dev/lib/file_system" />
<excludeFolder url="file://$MODULE_DIR$/_build/dev/lib/jason" />
<excludeFolder url="file://$MODULE_DIR$/_build/test/lib/jason" />
<excludeFolder url="file://$MODULE_DIR$/_build/test/lib/bunt" />
<excludeFolder url="file://$MODULE_DIR$/_build/test/lib/file_system" />
<excludeFolder url="file://$MODULE_DIR$/_build/test/lib/credo" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="x_util" />
<orderEntry type="library" name="assertions" level="project" />
<orderEntry type="library" name="socket" level="project" />
<orderEntry type="module" module-name="x_util" />
<orderEntry type="module" module-name="x_util" />
<orderEntry type="library" name="x_util" level="project" />
<orderEntry type="module" module-name="x_util" />
<orderEntry type="library" name="comb" level="project" />
<orderEntry type="library" name="ex_doc" level="project" />
<orderEntry type="library" name="ecto" level="project" />
<orderEntry type="library" name="absinthe" level="project" />
<orderEntry type="library" name="credo" level="project" />
<orderEntry type="library" name="file_system" level="project" />
<orderEntry type="library" name="bunt" level="project" />
<orderEntry type="library" name="jason" level="project" />
<orderEntry type="library" name="excoveralls" level="project" />
<orderEntry type="library" name="inch_ex" level="project" />
</component>
</module>
================================================
FILE: test/connection_test.exs
================================================
defmodule RakNet.ConnectionTest do
use ExUnit.Case, async: true
doctest RakNet.Connection
import RakNet.Connection, only: [message_indices_from_ack: 1]
require Assertions
import Assertions, only: [assert_lists_equal: 2]
@min_is_max 1
@min_is_not_max 0
@packet_count_1 1
test "handles single acks" do
assert [2] == message_indices_from_ack(<<@packet_count_1::size(16), @min_is_max::size(8), 2::little-size(24)>>)
assert message_indices_from_ack(<<0, 1, 1, 0, 0, 0>>) == [0]
assert message_indices_from_ack(<<0, 1, 1, 1, 0, 0>>) == [1]
assert message_indices_from_ack(<<0, 1, 1, 3, 0, 0>>) == [3]
end
test "handles ack range" do
assert_lists_equal(
[1, 2, 3, 4],
message_indices_from_ack(<<@packet_count_1::size(16), @min_is_not_max::size(8), 1::little-size(24), 4::little-size(24)>>)
)
assert_lists_equal([1, 2], message_indices_from_ack(<<0, 1, 0, 1, 0, 0, 2, 0, 0>>))
end
test "handles many disjoint acks" do
assert_lists_equal(
[0, 4, 5, 6, 8, 9, 10, 11, 48],
message_indices_from_ack(<<
# Packet count
4::size(16),
# Drop 0
@min_is_max::size(8),
0::little-size(24),
# Drop 4, 5, 6
@min_is_not_max::size(8),
4::little-size(24),
6::little-size(24),
# Drop 8-11, inclusive
@min_is_not_max::size(8),
8::little-size(24),
11::little-size(24),
# Drop 48
@min_is_max::size(8),
48::little-size(24)
>>)
)
end
end
================================================
FILE: test/message_test.exs
================================================
defmodule RakNet.MessageTest do
use ExUnit.Case, async: true
test "lists the message name atoms" do
known_msg_names = RakNet.Message.known_message_names()
expected_atoms = MapSet.new([:ping, :unconnected_ping, :pong, :client_disconnect, :data_packet_8, :ack])
assert MapSet.subset?(expected_atoms, known_msg_names)
end
test "lists the message binary values" do
known_msgs = RakNet.Message.known_messages()
expected_vals = MapSet.new([0, 1, 2, 3, 5, 6, 7, 8, 9, 0x10, 0x13, 0x15, 0x1C, 0x1D, 0x80, 0x8F, 0xA0, 0xC0])
assert MapSet.subset?(expected_vals, known_msgs)
expected_missing = MapSet.new([4, 0x0A, 0x12, 0x14, 0x1B, 0x1E])
assert MapSet.disjoint?(known_msgs, expected_missing)
assert Enum.all?(Enum.map(expected_vals, &RakNet.Message.is_known?/1))
assert !Enum.any?(Enum.map(expected_missing, &RakNet.Message.is_known?/1))
end
test "fetches message name atoms from binary values" do
assert RakNet.Message.name(0) == :ping
assert RakNet.Message.name(0x13) == :client_handshake
assert RakNet.Message.name(0x13) == :client_handshake
assert RakNet.Message.name(0x14) == :error
assert RakNet.Message.name(0x1B) == :error
end
end
================================================
FILE: test/packet_test.exs
================================================
defmodule RakNet.PacketTest do
use ExUnit.Case
@test_data_packet <<25, 62, 69, 23, 124, 1, 0, 32, 0, 224, 105, 1, 0, 1, 0, 0, 0, 144, 9, 89, 211, 245, 50, 93, 224, 84, 166, 81, 195, 12,
94, 253, 127, 183, 0, 128, 255, 127, 255, 127, 195, 56, 128, 64, 0>>
@tag packet: true
test "decodes unreliable sequenced data packets" do
assert %{encapsulated_packets: [packet | []]} = RakNet.Packet.decode_with_timestamp(@test_data_packet)
assert packet.priority == 4
assert packet.reliability == :unreliable_sequenced
assert packet.length == 28
assert is_nil(packet.message_index)
assert not is_nil(packet.sequencing_index)
end
end
================================================
FILE: test/server_test.exs
================================================
defmodule RakNet.ServerTest do
use ExUnit.Case
require Logger
import RakNet.Packet, only: [decode_with_timestamp: 1, decode_no_timestamp: 1]
@moduledoc """
This is an acceptance test (in the sense popularized in the C++ community by Clare Macrae.
We run through the exact sequence of packets that an official RakNet sample app sends, and assert
that we send the correct responses.
"""
# I've hard-coded this as my GUID in my copy of the RakNet code, so that I can create reproducible packet tests
@fixed_id 12_345_678_901_234_567_890
@localhost {127, 0, 0, 1}
@localhost6 {0, 0, 0, 0, 0, 0, 0, 1}
test "handles client connection negotiation" do
{_server_pid, server_host_and_port} = start_server(49_101)
{client_send, client_send_padded} = make_client_send_fns(49_100, server_host_and_port)
# :open_connection_request_1 -> :open_connection_reply_1
open_conn_req_1_with_retries(client_send_padded, fn -> server_responded("0600ffff00fefefefefdfdfdfd12345678ab54a98ceb1f0ad20005d4") end)
# :open_connection_request_2 -> :open_connection_reply_2
client_send.("0700ffff00fefefefefdfdfdfd123456780480fffffebfcd05d400059bb99c3c475c")
assert server_responded("0800ffff00fefefefefdfdfdfd12345678ab54a98ceb1f0ad20480fffffebfcc05d400", normalize_ip_addresses: true),
"Failed to respond to open connection request 2"
# :data_packet_4[:client_connect] -> :data_packet_4[:server_handshake]
client_send.("840000004001080000000900059bb98e0d2f4a00000000000000160052756d70656c7374696c74736b696e")
assert server_responded_many([
{"8400000060030000000000000000100480fffffebfcc0000043f57fe32bfcd04ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000000000000000000160000000000004345",
[
normalize_ip_addresses: true,
# Ignore the timestamp at the end
discard_last_bytes: 4
]},
{"c0000101000000", []}
]),
"Failed to respond to client_connect with handshake and/or ack client ordered packet 0"
# Have the client ack the handshake
client_send.("c0000101000000")
# :data_packet_4[client_handshake] requires a pong response from its ping
client_send.(
"840100006002f001000000000000130480fffffebfcd043f57fe32bfcc04ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000000000000000040160000000000000025000048000000000000000025"
)
# :data_packet_4[ping]
client_send.("8402000000004800000000000000002f")
# :data_packet_4[ping, pong for 0x25, pong for 0x2f]
# "8401000000004800000000000000402d000088030000000000000025000000000000402d00008803000000000000002f000000000000402d",
assert server_sent_packets_with(2, [
{:ack, [1, 2]},
{:ping, :unreliable},
{:pong, :unreliable},
{:pong, :unreliable},
{:pong, :unreliable}
]),
"Failed to handle client handshake and additional enqueued pong"
# Ack the data packet with the ping and 2 pongs
client_send.("c0000101010000")
# 2 pongs to respond to our pings
client_send.("84030000000088030000000000003ca4000000000000002c000088030000000000003ca4000000000000002c")
assert server_responded("c0000101030000"), "Failed to respond to ack client's packet 3"
# Send an actual "business logic" packet---a chat message "ahoy"
client_send.("841a00006000280800000200000061686f7900")
# Note that the "a" gets dropped from our "ahoy" packet, because the first byte of the message is always the client packet type
assert receive_business_logic_msg() == "hoy" <> <<0>>, "Failed to receive 'ahoy' business logic message"
end
@tag xplane: true
test "handles client negotiation with embedded timestamps" do
has_embedded_timestamp = true
# Send timestamps with datagrams!
{_server_pid, server_host_and_port} = start_xplane_server(49_108)
{client_send, client_send_padded} = make_client_send_fns(49_109, server_host_and_port)
# :open_connection_request_1 -> :open_connection_reply_1
open_conn_req_1_with_retries(client_send_padded, fn -> server_sent_packet_with({:open_connection_reply_1, :connection_negotiation}) end)
# :open_connection_request_2 -> :open_connection_reply_2
client_send.("0700FFFF00FEFEFEFEFDFDFDFD12345678043F57FE07BB8005D422A2C5FD8AF0CF58")
assert server_sent_packet_with({:open_connection_reply_2, :connection_negotiation}), "Failed to respond to open connection request 2"
# :data_packet_4[:client_connect] -> :data_packet_4[:server_handshake]
client_send.("840001703F0000004000900000000922A2C5FD8AF0CF58000000000000005D00")
assert server_sent_packets_with(2, [{:server_handshake, :reliable_ordered}, {:ack, [0]}], true),
"Failed to respond to client_connect with handshake and/or ack client ordered packet 0"
# Have the client ack the handshake
client_send.("C000000020000101000000")
# :data_packet_4[client_handshake] requires a pong response from its ping
client_send.(
"84000216EE0100006019E00100000000000013043F57FE07BB80061C1EBF6800000000FE80000000000000C2A53EFFFE188D771F000000061C1EBF6800000000FE8000000000000018AB2FDACB39D7F202000000061C1EBF6800000000FE80000000000000CBAD3755438672141C000000061C1EBF68000000002607FC20E12A44EBE8F61BF2372CAB6500000000061C1EBF6800000000FE80000000000000DD135BB99B71EB3216000000061C1EBF68000000002607FC20E12A44EB10A1ABB78F934D9300000000061C1EBF6800000000FE800000000000004FD46183DE0250B315000000061C1EBF68000000002607FC20E12A44EBD143E3D178F8727800000000061C1EBF68000000002607FC20E12A44EBE497120B5A43099E00000000061C1EBF68000000002607FB901787903B319202F400D6266300000000061C1EBF6800000000FE8000000000000000381638AF01F1F703000000061C1EBF6800000000FE80000000000000590E28A7A8EE2D3514000000061C1EBF6800000000FE80000000000000397893EEA9E580A113000000049B69AB37BF68061C1EBF68000000002607FB901787903B043F38E29EE8C16000000000061C1EBF68000000002607FB901787903B097329AED1ACD39B00000000061C1EBF6800000000FE8000000000000004A607634B35B90308000000061C1EBF68000000002605A601AD7804000CE83F74A5CB264500000000061C1EBF6800000000FE80000000000000C2A53EFFFE188D770C000000061C1EBF6800000000FE80000000000000C017C6FFFE53F8660B000000061C1EBF6800000000FE80000000000000040018EFABB6CE8A0A000000061C1EBF6800000000FD7465726D6E7573000DA8FCEB6EE7BF00000000061C1EBF6800000000FE800000000000001AAA9D708BA12BB30D000000061C1EBF6800000000FD7465726D6E7573000CA8FCEB6EE7BF00000000061C1EBF68000000002607FB901787903B88E0C05F76E6D09900000000061C1EBF68000000002607FB901787903B802F37306E7ABD4200000000061C1EBF68000000002605A601AD780400A4F5EA8182BC7797000000000456018792BF68043F57FE35BF68061C1EBF6800000000FD7465726D6E7573000CA8FCEB6EE7BF000000000000000000000000000000000000007000004800000000000000007D"
)
# :data_packet_4[ping, pong for 0x25, pong for 0x2f]
# "8401000000004800000000000000402d000088030000000000000025000000000000402d00008803000000000000002f000000000000402d",
assert server_sent_packets_with(
2,
[
{:ack, [1]},
{:ping, :unreliable},
{:pong, :unreliable},
{:pong, :unreliable}
],
has_embedded_timestamp
),
"Failed to handle client handshake and additional enqueued pong"
end
@tag slow: true
test "resends unacknowledged packets" do
# ------------------------------------------------------------------------------------------------------------------
# BEGIN COPYPASTA
# Everything up to the Process.sleep() is copypasta from the connection negotiation test!
# ------------------------------------------------------------------------------------------------------------------
{_server_pid, server_host_and_port} = start_server(49_103)
{client_send, client_send_padded} = make_client_send_fns(49_102, server_host_and_port)
# :open_connection_request_1 -> :open_connection_reply_1
open_conn_req_1_with_retries(client_send_padded, fn -> server_sent_packet_with({:open_connection_reply_1, :connection_negotiation}) end)
# :open_connection_request_2 -> :open_connection_reply_2
client_send.("0700ffff00fefefefefdfdfdfd123456780480fffffebfcf05d400059bb99c3c475c")
assert server_sent_packet_with({:open_connection_reply_2, :connection_negotiation}), "Failed to respond to open connection request 2"
# :data_packet_4[:client_connect] -> :data_packet_4[:server_handshake]
client_send.("840000004001080000000900059bb98e0d2f4a00000000000000160052756d70656c7374696c74736b696e")
assert server_sent_packets_with(2, [{:server_handshake, :reliable_ordered}, {:ack, [0]}]),
"Failed to respond to client_connect with handshake and/or ack client ordered packet 0"
# ------------------------------------------------------------------------------------------------------------------
# END COPYPASTA
# ------------------------------------------------------------------------------------------------------------------
# Sleep while the client deliberately does *not* send an ack for the :server_handshake packet
Process.sleep(1100)
assert server_sent_packet_with({:server_handshake, :reliable_ordered}), "Failed to resend reliable packet :server_handshake"
Process.sleep(1100)
assert server_sent_packet_with({:server_handshake, :reliable_ordered}), "Failed to resend :server_handshake a second time"
# Ack the handshake (packet index 2, not 0, since reliable packet 0 got resent twice)
# TODO: Should we be keeping track of the packet's *old* index and allowing ack based on that? (0 in this case)
client_send.("c0000101020000")
receive do
{:protocol_msg, msg_raw} -> raise RuntimeError, message: "Server should not have sent any more messages; sent #{inspect(msg_raw)}"
after
1_100 -> :ok
end
end
@tag slow: true
test "connections time out after not hearing from the client" do
timeout_ms = 500
{_server_pid, server_host_and_port} = start_server(49_105, timeout_ms)
{_client_send, client_send_padded} = make_client_send_fns(49_104, server_host_and_port)
# :open_connection_request_1 -> :open_connection_reply_1
open_conn_req_1_with_retries(client_send_padded, fn -> server_sent_packet_with({:open_connection_reply_1, :connection_negotiation}) end)
:timer.sleep(timeout_ms + 100)
matching_pids = Registry.lookup(RakNet.Connection, {@localhost, 49_104})
if not Enum.empty?(matching_pids) do
[{_, pid} | _] = matching_pids
if Process.alive?(pid) do
# Flush all messages on the process
:sys.get_state(pid)
end
assert not Process.alive?(pid)
end
end
defmodule AckRequestingClient do
@enforce_keys [:handle_data, :handle_ack, :connection_pid]
defstruct handle_data: nil, handle_ack: nil, connection_pid: nil
def new(connection_pid, test_pid) do
:timer.apply_interval(100, AckRequestingClient, :bug_the_user, [connection_pid])
%AckRequestingClient{
connection_pid: connection_pid,
handle_data: fn _packet_type, data -> send(test_pid, {:business_logic_msg, data}) end,
handle_ack: fn send_receipt_id -> send(test_pid, {:business_logic_ack, send_receipt_id}) end
}
end
def bug_the_user(connection_pid) do
RakNet.Connection.send(connection_pid, :reliable_ack_receipt, "please acknowledge")
end
end
defimpl RakNet.Client, for: AckRequestingClient do
def new(_client_struct, connection_pid, test_pid), do: AckRequestingClient.new(connection_pid, test_pid)
def receive(client, packet_type, packet_buffer, _time_comp), do: client.handle_data.(packet_type, packet_buffer)
def got_ack(client, send_receipt_id), do: client.handle_ack.(send_receipt_id)
def disconnect(_client), do: :ok
end
test "forwards acks to game logic client" do
# ------------------------------------------------------------------------------------------------------------------
# BEGIN COPYPASTA
# Everything up to the Process.sleep() is copypasta from the connection negotiation test!
# ------------------------------------------------------------------------------------------------------------------
{_server_pid, server_host_and_port} = start_server(49_201, 1_000, AckRequestingClient)
{client_send, client_send_padded} = make_client_send_fns(49_200, server_host_and_port)
# :open_connection_request_1 -> :open_connection_reply_1
open_conn_req_1_with_retries(client_send_padded, fn -> server_sent_packet_with({:open_connection_reply_1, :connection_negotiation}) end)
# :open_connection_request_2 -> :open_connection_reply_2
client_send.("0700ffff00fefefefefdfdfdfd123456780480fffffebfcd05d400059bb99c3c475c")
assert server_sent_packet_with({:open_connection_reply_2, :connection_negotiation}), "Failed to respond to open connection request 2"
# :data_packet_4[:client_connect] -> :data_packet_4[:server_handshake]
client_send.("840000004001080000000900059bb98e0d2f4a00000000000000160052756d70656c7374696c74736b696e")
assert server_sent_packets_with(2, [{:server_handshake, :reliable_ordered}, {:ack, [0]}]),
"Failed to respond to client_connect with handshake and/or ack client ordered packet 0"
# Have the client ack the handshake
client_send.("c0000101000000")
# :data_packet_4[client_handshake] requires a pong response from its ping
client_send.(
"840100006002f001000000000000130480fffffebfcd043f57fe32bfcc04ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000000000000000040160000000000000025000048000000000000000025"
)
# :data_packet_4[ping]
client_send.("8402000000004800000000000000002f")
# :data_packet_4[ping, pong for 0x25, pong for 0x2f]
assert server_sent_packets_with(2, [
{:ack, [1, 2]},
{:ping, :unreliable},
{:pong, :unreliable},
{:pong, :unreliable},
{:pong, :unreliable}
]),
"Failed to handle client handshake and additional enqueued pong"
# Ack the data packet with the ping and 2 pongs
client_send.("c0000101010000")
# 2 pongs to respond to our pings
client_send.("84030000000088030000000000003ca4000000000000002c000088030000000000003ca4000000000000002c")
assert server_responded("c0000101030000"), "Failed to respond to ack client's packet 3"
# ------------------------------------------------------------------------------------------------------------------
# END COPYPASTA
# ------------------------------------------------------------------------------------------------------------------
# Have the server send a value that requests an ack
assert server_sent_packet_with({"please acknowledge", :reliable_ack_receipt})
client_send.("c0000101020000")
assert receive_business_logic_ack()
end
@tag ipv6: true
test "handles IPv6 connections" do
{_server_pid, _server_host_and_port} = start_server(49_211, 1_000, AckRequestingClient)
{client_send, client_send_padded} = make_client_send_fns(49_210, {@localhost6, 49_211}, 6)
# :open_connection_request_1 -> :open_connection_reply_1
open_conn_req_1_with_retries(client_send_padded, fn -> server_sent_packet_with({:open_connection_reply_1, :connection_negotiation}) end)
# :open_connection_request_2 -> :open_connection_reply_2
client_send.("0700ffff00fefefefefdfdfdfd123456780480fffffebfcd05d400059bb99c3c475c")
assert server_sent_packet_with({:open_connection_reply_2, :connection_negotiation}), "Failed to respond to open connection request 2"
# :data_packet_4[:client_connect] -> :data_packet_4[:server_handshake]
client_send.("840000004001080000000900059bb98e0d2f4a00000000000000160052756d70656c7374696c74736b696e")
assert server_sent_packets_with(2, [{:server_handshake, :reliable_ordered}, {:ack, [0]}]),
"Failed to respond to client_connect with handshake and/or ack client ordered packet 0"
# Have the client ack the handshake
client_send.("c0000101000000")
# :data_packet_4[client_handshake] requires a pong response from its ping
client_send.(
"840100006002f001000000000000130480fffffebfcd043f57fe32bfcc04ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000004ffffffff000000000000000040160000000000000025000048000000000000000025"
)
# :data_packet_4[ping]
client_send.("8402000000004800000000000000002f")
# :data_packet_4[ping, pong for 0x25, pong for 0x2f]
assert server_sent_packets_with(2, [
{:ack, [1, 2]},
{:ping, :unreliable},
{:pong, :unreliable},
{:pong, :unreliable},
{:pong, :unreliable}
]),
"Failed to handle client handshake and additional enqueued pong"
# Ack the data packet with the ping and 2 pongs
client_send.("c0000101010000")
# 2 pongs to respond to our pings
client_send.("84030000000088030000000000003ca4000000000000002c000088030000000000003ca4000000000000002c")
assert server_responded("c0000101030000"), "Failed to respond to ack client's packet 3"
# Have the server send a value that requests an ack
assert server_sent_packet_with({"please acknowledge", :reliable_ack_receipt})
client_send.("c0000101020000")
assert receive_business_logic_ack()
end
test "server complains if you send data packets without having negotiated a connection" do
{_server_pid, server_host_and_port} = start_server(49_107)
{client_send, client_send_padded} = make_client_send_fns(49_106, server_host_and_port)
# Attempt to ack some non-existent packet
client_send.("c0000101010000")
assert server_sent_packet_with({:connection_lost, :connection_negotiation}), "Server should nag to establish a connection before ack"
# Send a data packet out of the blue
client_send.("84030000000088030000000000003ca4000000000000002c000088030000000000003ca4000000000000002c")
assert server_sent_packet_with({:connection_lost, :connection_negotiation}), "Server should nag connect before sending data"
# Start opening a connection, but then send a data packet before completing the connection negotiation
open_conn_req_1_with_retries(client_send_padded, fn -> server_sent_packet_with({:open_connection_reply_1, :connection_negotiation}) end)
client_send.("841a00006000280800000200000061686f7900")
assert server_sent_packet_with({:connection_lost, :connection_negotiation}), "Server should nag to finish connection negotiation"
end
test "server survives connection crash" do
{server_pid, server_host_and_port} = start_server(49_900)
# Start a bunch of connections
[{live_client_send, port_to_keep_alive} | send_fns_and_ports_to_kill] =
Enum.map(1..10, fn i ->
port = 49_900 + i
{client_send, client_send_padded} = make_client_send_fns(port, server_host_and_port)
# :open_connection_request_1 -> :open_connection_reply_1
open_conn_req_1_with_retries(client_send_padded, fn ->
server_sent_packet_with({:open_connection_reply_1, :connection_negotiation})
end)
{client_send, port}
end)
ports_to_kill = Enum.map(send_fns_and_ports_to_kill, fn {_, port} -> port end)
# Kill all but one
Enum.each(ports_to_kill, fn port ->
[{_, pid}] = Registry.lookup(RakNet.Connection, {@localhost, port})
Process.exit(pid, :kill)
assert not Process.alive?(pid), "Failed to kill process for port #{port}"
end)
assert Process.alive?(server_pid), "Server died when we killed its connections"
[{_, live_pid}] = Registry.lookup(RakNet.Connection, {@localhost, port_to_keep_alive})
:sys.get_state(live_pid)
assert Process.alive?(live_pid), "PID died unexpectedly"
# :open_connection_request_2 -> :open_connection_reply_2
live_client_send.("0700ffff00fefefefefdfdfdfd123456780480fffffebfcd05d400059bb99c3c475c")
assert server_sent_packet_with({:open_connection_reply_2, :connection_negotiation}), "Failed to respond to open connection request 2"
assert Process.alive?(server_pid), "Server died when we killed its connections"
end
defmodule DummyClient do
defstruct handle_data: nil
def accept_data(client, packet_type, buffer) do
client.handle_data.(packet_type, buffer)
client
end
end
defimpl RakNet.Client, for: DummyClient do
def new(_, _, test_pid), do: %DummyClient{handle_data: fn _type, data -> send(test_pid, {:business_logic_msg, data}) end}
def receive(client, packet_type, packet_buffer, _time_comp), do: DummyClient.accept_data(client, packet_type, packet_buffer)
def got_ack(client, _send_receipt_id), do: client
def disconnect(_client), do: :ok
end
defp start_server(port, timeout_ms \\ 10_000, client_module \\ nil) do
test_pid = self()
{:ok, server_pid} =
RakNet.Server.start_link(if(client_module, do: client_module, else: DummyClient), port,
client_timeout_ms: timeout_ms,
client_data: test_pid,
server_identifier: <<@fixed_id::size(64)>>,
host: @localhost,
open_ipv6_socket: System.get_env("SEPARATE_IPV6_PORT") != "false",
send: fn _, packet, _client ->
send(test_pid, {:protocol_msg, packet})
end
)
{server_pid, {@localhost, port}}
end
defp start_xplane_server(port) do
test_pid = self()
{:ok, server_pid} =
RakNet.Server.start_link(DummyClient, port,
client_data: test_pid,
server_identifier: <<@fixed_id::size(64)>>,
host: @localhost,
send: fn _, packet, _client -> send(test_pid, {:protocol_msg, packet}) end,
# Begin X-Plane-specific #defines!
include_timestamp_with_datagrams: true,
max_number_of_internal_ids: 30
)
{server_pid, {@localhost, port}}
end
defp make_client_send_fns(client_port, server_host_and_port, ip_version \\ 4) do
socket =
if ip_version == 4 do
Socket.UDP.open!(client_port, local: [address: @localhost], version: 4)
else
Socket.UDP.open!(client_port, local: [address: @localhost6], version: 6)
end
client_send_padded = fn packet_string, pad_to_byte_length ->
data = packet_decode(packet_string)
pad_length = max(pad_to_byte_length * 8 - bit_size(data), 0)
Socket.Datagram.send!(socket, data <> <<0::size(pad_length)>>, server_host_and_port)
end
client_send = fn packet_string -> client_send_padded.(packet_string, 0) end
{client_send, client_send_padded}
end
defp packet_decode(packet_hex_string, pad_to_byte_length \\ 0) do
data = Base.decode16!(packet_hex_string, case: :mixed)
pad_length = max(pad_to_byte_length * 8 - bit_size(data), 0)
data <> <<0::size(pad_length)>>
end
defp server_responded(packet_hex_string, opts \\ []) when is_binary(packet_hex_string) do
server_responded_many([{packet_hex_string, opts}])
end
defp server_sent_packet_with(packet_spec, has_embedded_timestamp \\ false) do
server_sent_packets_with(1, [packet_spec], has_embedded_timestamp)
end
defp server_sent_packets_with(num_packets, packet_spec, has_embedded_timestamp \\ false) when is_list(packet_spec) do
actual_types_and_reliabilities =
num_packets
|> receive_n_sends()
|> Enum.flat_map(&parse_packet_type(&1, has_embedded_timestamp))
if Enum.sort(packet_spec) == Enum.sort(actual_types_and_reliabilities) do
true
else
Logger.error("Expected packets: #{inspect(packet_spec, limit: :infinity)}")
Logger.error("Received packets: #{inspect(actual_types_and_reliabilities, limit: :infinity)}")
false
end
end
# credo:disable-for-next-line
defp parse_packet_type(<<type::size(8), remainder::binary>>, has_embedded_timestamp) do
ack = RakNet.Message.binary(:ack)
decode_fn = if has_embedded_timestamp, do: &decode_with_timestamp/1, else: &decode_no_timestamp/1
ack_timestamp_bits = if has_embedded_timestamp, do: 32, else: 0
case type do
^ack ->
[{:ack, RakNet.Connection.message_indices_from_ack(drop_leading_bits(remainder, ack_timestamp_bits))}]
x when x in 0x80..0x8F ->
Enum.map(decode_fn.(remainder)[:encapsulated_packets], fn packet ->
<<encapsulated_type::size(8), _::binary>> = packet.buffer
case RakNet.Message.name(encapsulated_type) do
:error -> {packet.buffer, packet.reliability}
name -> {name, packet.reliability}
end
end)
x when x in 0x05..0x16 ->
[{RakNet.Message.name(type), :connection_negotiation}]
end
end
defp server_responded_many(packet_specs) when is_list(packet_specs) do
packets_and_xforms =
Enum.map(packet_specs, fn {packet_hex_string, opts} ->
# Replace loopback adapter's IP+port combo (which is what happens when you run the RakNet samples in the terminal)
# with "plain-old localhost" (which is what we get when we run the ExUnit test)
normalize_ips =
if Keyword.get(opts, :normalize_ip_addresses, false) do
fn bits ->
bits
|> String.replace(<<128, 255, 255, 254, 191, 204>>, <<127, 0, 0, 1, 191, 204>>)
|> String.replace(<<128, 255, 255, 254, 191, 206>>, <<127, 0, 0, 1, 191, 206>>)
|> String.replace(<<63, 87, 254, 50, 191, 205>>, <<127, 0, 0, 1, 191, 205>>)
|> String.replace(<<63, 87, 254, 50, 191, 207>>, <<127, 0, 0, 1, 191, 207>>)
end
else
& &1
end
drop_trailing = fn bits ->
new_length_bytes = byte_size(bits) - Keyword.get(opts, :discard_last_bytes, 0)
if new_length_bytes > 2, do: <<bits::binary-size(new_length_bytes)>>, else: bits
end
drop_bits = Keyword.get(opts, :discard_first_bytes, 0) * 8
drop_leading = &drop_leading_bits(&1, drop_bits)
transform = fn bits -> bits |> drop_leading.() |> drop_trailing.() |> normalize_ips.() end
expected_raw = packet_decode(packet_hex_string, Keyword.get(opts, :pad_to_byte_length, 0))
{expected_raw, transform.(expected_raw), transform}
end)
reduce_remove_nil = fn x, acc -> if x, do: [x | acc], else: acc end
msgs_raw = receive_n_sends(length(packets_and_xforms))
missing_expected =
packets_and_xforms
|> Enum.map(fn {raw, expected, transform} ->
has_match = Enum.any?(msgs_raw, fn msg_raw -> transform.(msg_raw) == expected end)
if has_match, do: nil, else: raw
end)
|> Enum.reduce([], reduce_remove_nil)
if length(missing_expected) > 0 do
Logger.error("Failed to receive expected message(s):")
Enum.each(missing_expected, fn msg -> Logger.error("#{inspect(msg, limit: :infinity)}") end)
end
unexpected_msgs =
msgs_raw
|> Enum.map(fn msg_raw ->
matched = Enum.any?(packets_and_xforms, fn {_raw, expected, transform} -> transform.(msg_raw) == expected end)
if matched, do: nil, else: msg_raw
end)
|> Enum.reduce([], reduce_remove_nil)
if length(unexpected_msgs) > 0 do
Logger.error("Received unexpected message(s):")
Enum.each(unexpected_msgs, fn msg -> Logger.error("#{inspect(msg, limit: :infinity)}") end)
end
Enum.empty?(unexpected_msgs) and Enum.empty?(missing_expected)
end
defp drop_leading_bits(data, num_bits) do
<<_::size(num_bits), remainder::binary>> = data
remainder
end
# With enough tests going at once, we can overwhelm the network interface---it's okay if we have to try this again
defp open_conn_req_1_with_retries(client_send_padded, success_check, max_attempts \\ 10, attempt \\ 1)
defp open_conn_req_1_with_retries(client_send_padded, success_check, max_attempts, attempt) when max_attempts > attempt do
client_send_padded.("0500ffff00fefefefefdfdfdfd1234567806", 1464)
assert success_check.()
rescue
_ ->
Process.sleep(250 + :rand.uniform(1250))
open_conn_req_1_with_retries(client_send_padded, success_check, max_attempts, attempt + 1)
end
defp open_conn_req_1_with_retries(_client_send_padded, _success_check, _max_attempts, _attempt) do
raise RuntimeError, message: "Failed to receive open_connection_reply_1 from our open_connection_request_1"
end
def receive_n_sends(n) when is_integer(n) do
Enum.map(1..n, fn _ ->
receive do
{:protocol_msg, msg_raw} -> msg_raw
after
1_000 ->
raise RuntimeError, message: "Didn't receive enough messages"
end
end)
end
def receive_business_logic_msg do
receive do
{:business_logic_msg, msg_raw} -> msg_raw
after
5_000 ->
raise RuntimeError, message: "Didn't receive a business logic message"
end
end
def receive_business_logic_ack do
receive do
{:business_logic_ack, msg_raw} -> msg_raw
after
5_000 ->
raise RuntimeError, message: "Didn't receive a business logic ack"
end
end
end
================================================
FILE: test/system_address_test.exs
================================================
defmodule RakNet.SystemAddressTest do
use ExUnit.Case, async: true
import Assertions, only: [assert_lists_equal: 2]
test "decodes IPv4 addresses" do
assert_lists_equal(RakNet.SystemAddress.decode_many(<<4, 63, 87, 254, 7, 187, 128, 4, 63, 87, 254, 53, 191, 104>>), [
%{version: 4, address: {63, 87, 254, 7}, port: 48_000},
%{version: 4, address: {63, 87, 254, 53}, port: 49_000}
])
end
test "decodes IPv6 addresses" do
# v6 size fam port flow info address------------------------------------------------------- scope
ipv6_1 = <<6, 28, 30, 191, 104, 0, 0, 0, 0, 254, 128, 0, 0, 0, 0, 0, 0, 4, 0, 24, 239, 171, 182, 206, 138, 10, 0, 0, 0>>
ipv6_2 = <<6, 28, 30, 191, 105, 0, 0, 0, 0, 38, 7, 251, 144, 23, 135, 144, 59, 213, 146, 204, 243, 194, 122, 129, 248, 0, 0, 0, 0>>
assert_lists_equal(RakNet.SystemAddress.decode_many(ipv6_1 <> ipv6_2), [
%{version: 6, address: {65_152, 0, 0, 0, 1024, 6383, 43_958, 52_874}, port: 49_000},
%{version: 6, address: {9735, 64_400, 6023, 36_923, 54_674, 52_467, 49_786, 33_272}, port: 49_001}
])
end
test "decodes a mix of IPv4 and IPv6 addresses" do
ipv4_1 = <<4, 63, 87, 254, 7, 187, 128>>
ipv4_2 = <<4, 63, 87, 254, 53, 191, 104>>
ipv6_1 = <<6, 28, 30, 191, 104, 0, 0, 0, 0, 254, 128, 0, 0, 0, 0, 0, 0, 4, 0, 24, 239, 171, 182, 206, 138, 10, 0, 0, 0>>
ipv6_2 = <<6, 28, 30, 191, 105, 0, 0, 0, 0, 38, 7, 251, 144, 23, 135, 144, 59, 213, 146, 204, 243, 194, 122, 129, 248, 0, 0, 0, 0>>
assert_lists_equal(RakNet.SystemAddress.decode_many(ipv6_1 <> ipv4_1 <> ipv6_2 <> ipv4_2), [
%{version: 4, address: {63, 87, 254, 7}, port: 48_000},
%{version: 4, address: {63, 87, 254, 53}, port: 49_000},
%{version: 6, address: {65_152, 0, 0, 0, 1024, 6383, 43_958, 52_874}, port: 49_000},
%{version: 6, address: {9735, 64_400, 6023, 36_923, 54_674, 52_467, 49_786, 33_272}, port: 49_001}
])
assert_lists_equal(
RakNet.SystemAddress.decode_many(ipv6_1 <> ipv4_1 <> ipv6_2 <> ipv4_2),
RakNet.SystemAddress.decode_many(ipv4_1 <> ipv6_1 <> ipv4_2 <> ipv6_2)
)
assert_lists_equal(
RakNet.SystemAddress.decode_many(ipv6_1 <> ipv4_1 <> ipv6_2 <> ipv4_2),
RakNet.SystemAddress.decode_many(ipv6_2 <> ipv6_1 <> ipv4_2 <> ipv4_1)
)
end
end
================================================
FILE: test/test_helper.exs
================================================
ExUnit.start()
gitextract_ft9zctf5/
├── .circleci/
│ └── config.yml
├── .formatter.exs
├── .github/
│ └── ISSUE_TEMPLATE/
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .idea/
│ └── runConfigurations/
│ ├── Format.xml
│ └── Test.xml
├── LICENSE
├── README.md
├── config/
│ ├── .credo.exs
│ └── credo_checks/
│ └── explicitly_ignore_return_values.ex
├── lib/
│ ├── application.ex
│ ├── connection.ex
│ ├── message.ex
│ ├── packet.ex
│ ├── reliability_layer.ex
│ ├── server.ex
│ └── system_address.ex
├── mix.exs
├── raknet.iml
└── test/
├── connection_test.exs
├── message_test.exs
├── packet_test.exs
├── server_test.exs
├── system_address_test.exs
└── test_helper.exs
SYMBOL INDEX (129 symbols across 14 files)
FILE: config/credo_checks/explicitly_ignore_return_values.ex
class ExplicitlyIgnoreReturnValues (line 1) | defmodule ExplicitlyIgnoreReturnValues
method explanations (line 46) | def explanations do
method param_defaults (line 57) | def param_defaults, do: [ignore: []]
method run (line 59) | def run(source_file, params \\ []) do
method issue_for (line 77) | defp issue_for(format_issue_fun, issue_meta, line_no, trigger) do
method find_unused_calls (line 92) | def find_unused_calls(%SourceFile{} = source_file, _params, funs_to_al...
method traverse_defs (line 108) | defp traverse_defs(ast, acc, _) do
method find_candidates (line 114) | defp find_candidates({{:., _, [{:__aliases__, _, modules}, function]},...
method find_candidates (line 123) | defp find_candidates(ast, acc, _) do
method filter_unused_calls (line 129) | defp filter_unused_calls(ast, candidates) do
method detect_unused_call (line 135) | defp detect_unused_call(candidate, ast) do
method verified_or_unused_call (line 141) | defp verified_or_unused_call(:VERIFIED, _), do: nil
method verified_or_unused_call (line 142) | defp verified_or_unused_call(_, candidate), do: candidate
method traverse_verify_candidate (line 146) | defp traverse_verify_candidate(ast, acc, candidate) do
method last_call_in_do_block? (line 169) | defp last_call_in_do_block?(ast, candidate) do
method last_call_in_rescue_block? (line 176) | defp last_call_in_rescue_block?(ast, candidate) do
method verify_candidate (line 198) | defp verify_candidate({:=, _, _} = ast, :not_verified = acc, candidate...
method verify_candidate (line 225) | defp verify_candidate(
method verify_candidate (line 334) | defp verify_candidate(ast, acc, _candidate) do
FILE: lib/application.ex
class RakNet.Application (line 1) | defmodule RakNet.Application
method start (line 6) | def start(_type, _args) do
FILE: lib/connection.ex
class RakNet.Connection (line 8) | defmodule RakNet.Connection
method start_link (line 103) | def start_link(%State{} = state) do
method stop (line 107) | def stop(connection_pid), do: GenServer.stop(connection_pid, :shutdown)
method handle_message (line 109) | def handle_message(connection_pid, message_type, data) do
method send (line 119) | def send(connection_pid, reliability, message)
method init (line 133) | def init(state) do
method handle_info (line 143) | def handle_info(:sync, connection) do
method handle_info (line 153) | def handle_info(:sync_ping, connection) do
method handle_info (line 158) | def handle_info({:EXIT, _pid, reason}, connection) do
method sync_ack_buffer (line 168) | defp sync_ack_buffer(connection) do
method sync_enqueued_data_packets (line 180) | defp sync_enqueued_data_packets(connection) do
method sync_requeue_reliable_data_packets (line 189) | defp sync_requeue_reliable_data_packets(connection) do
method ping (line 213) | defp ping(connection) do
method handle_cast (line 266) | def handle_cast({:open_connection_request_1, data}, connection) do
method handle_cast (line 290) | def handle_cast({:open_connection_request_2, data}, connection) do
method handle_cast (line 308) | def handle_cast({:ping, <<ping_time::size(64)>>}, connection) do
method handle_cast (line 313) | def handle_cast({:pong, <<our_sent_time::size(64), _::binary>>}, %Stat...
method handle_cast (line 320) | def handle_cast({:ack, packet}, %State{unacknowledged_sent: unacked} =...
method handle_cast (line 341) | def handle_cast({:nack, _packet}, connection) do
method handle_cast (line 347) | def handle_cast({:client_connect, data}, connection) do
method handle_cast (line 372) | def handle_cast({:client_handshake, data}, connection) do
method handle_cast (line 397) | def handle_cast({:client_disconnect, _data}, connection) do
method handle_cast (line 461) | def handle_cast({:send, reliability, message}, %State{} = connection) do
method handle_call (line 466) | def handle_call({:send, reliability, message}, _from, %State{} = conne...
method terminate (line 472) | def terminate(reason, connection) do
method reschedule_timeout (line 477) | defp reschedule_timeout(%State{timeout_ref: nil} = connection) do
method reschedule_timeout (line 484) | defp reschedule_timeout(%State{} = connection) do
method sweep_line (line 557) | def sweep_line(integers) do
method message_indices_from_ack (line 598) | def message_indices_from_ack(<<packet_count::size(16), remainder::bina...
method make_ping_buffer (line 620) | defp make_ping_buffer(base_time) do
method make_pong_buffer (line 624) | defp make_pong_buffer(ping_time, base_time) do
method rtt (line 629) | defp rtt(%State{last_rtts: []}), do: 200
method rtt (line 630) | defp rtt(%State{last_rtts: rtts}), do: Enum.sum(rtts) / length(rtts)
class State (line 18) | defmodule State
class Resendable (line 81) | defmodule Resendable
FILE: lib/message.ex
class RakNet.Message (line 1) | defmodule RakNet.Message
method known_message_names (line 62) | def known_message_names, do: @msg_names
method known_messages (line 68) | def known_messages, do: @msg_binary_vals
method offline_msg_id (line 85) | def offline_msg_id, do: <<0, 255, 255, 0, 254, 254, 254, 254, 253, 253...
FILE: lib/packet.ex
class RakNet.Packet (line 1) | defmodule RakNet.Packet
method decode_with_timestamp (line 7) | def decode_with_timestamp(<<timestamp::size(32), data::binary>>) do
method decode_no_timestamp (line 11) | def decode_no_timestamp(data, internal \\ false) do
method decode_no_timestamp (line 25) | defp decode_no_timestamp("", encapsulated_packets, _internal) do
method decode_no_timestamp (line 29) | defp decode_no_timestamp(rest, encapsulated_packets, internal) do
method encode (line 38) | def encode(%{sequence_number: seq_number, encapsulated_packets: encaps...
method decode_encapsulated_packet (line 42) | def decode_encapsulated_packet(data, internal) do
method encode_encapsulated_packet (line 100) | def encode_encapsulated_packet(%ReliabilityLayer.Packet{} = p, interna...
FILE: lib/reliability_layer.ex
class RakNet.ReliabilityLayer.Reliability (line 1) | defmodule RakNet.ReliabilityLayer.Reliability
class RakNet.ReliabilityLayer.Packet (line 42) | defmodule RakNet.ReliabilityLayer.Packet
method valid? (line 74) | def valid?(%RakNet.ReliabilityLayer.Packet{} = p) do
FILE: lib/server.ex
class RakNet.Server (line 1) | defmodule RakNet.Server
method child_spec (line 13) | def child_spec([receiver | [port | options]]) do
method timestamp (line 74) | def timestamp(offset \\ 0), do: unix_timestamp_ms() - offset
method make_unique_id (line 77) | def make_unique_id, do: <<timestamp()::size(48), :rand.uniform(65_536)...
method serve_v4 (line 79) | defp serve_v4({nil, _}, _config), do: :ok
method serve_v4 (line 80) | defp serve_v4({socket_v4, _} = sockets, config), do: serve_impl(&serve...
method serve_v6 (line 81) | defp serve_v6({_, nil}, _config), do: :ok
method serve_v6 (line 82) | defp serve_v6({_, socket_v6} = sockets, config), do: serve_impl(&serve...
method serve_impl (line 84) | defp serve_impl(server, receive_socket, sockets, config) do
method decode (line 106) | defp decode({packet, client_ip_and_port}, sockets, config) do
method open_connection (line 128) | defp open_connection(sockets, {host, port} = client_ip_and_port, confi...
method lookup (line 163) | defp lookup(client_ip_and_port) do
method packet_decode (line 178) | defp packet_decode(<<identifier::unsigned-size(8), data::binary>>, _co...
method handle_unconnected_packet (line 185) | defp handle_unconnected_packet(sockets, config, {client_ip_port, :unco...
method handle_unconnected_packet (line 189) | defp handle_unconnected_packet(sockets, config, {client_ip_port, :unco...
method handle_unconnected_packet (line 193) | defp handle_unconnected_packet(sockets, config, {client_ip_port, _othe...
method send_unconnected_pong (line 197) | defp send_unconnected_pong(sockets, client_ip_port, ping_time, config) do
method make_responder (line 207) | defp make_responder(sockets, config) do
method send (line 213) | defp send(sockets, packet, client_ip_and_port, config) do
method choose_socket (line 219) | defp choose_socket({socket_v4, socket_v6}, client_ip_and_port) do
class Spawner (line 8) | defmodule Spawner
method start_link (line 10) | def start_link(impl_fn), do: {:ok, spawn(impl_fn)}
FILE: lib/system_address.ex
class RakNet.SystemAddress (line 1) | defmodule RakNet.SystemAddress
method encode (line 4) | def encode(%{version: 4, address: {o1, o2, o3, o4}, port: port}),
method encode (line 7) | def encode(%{version: 6, address: {h1, h2, h3, h4, h5, h6, h7, h8}, po...
method encode (line 13) | def encode(%{address: addr, port: _port} = args) do
method ip_version (line 18) | def ip_version({_, _, _, _}), do: 4
method ip_version (line 19) | def ip_version({_, _, _, _, _, _, _, _}), do: 6
method decode_address_port (line 29) | defp decode_address_port(bin, prev \\ [])
method decode_address_port (line 31) | defp decode_address_port(<<4::size(8), address::binary-size(4), port::...
method decode_address_port (line 44) | defp decode_address_port(<<6::size(8), 28::unsigned-size(8), 30::unsig...
FILE: mix.exs
class RakNet.MixProject (line 1) | defmodule RakNet.MixProject
method project (line 4) | def project do
method application (line 19) | def application do
method deps (line 26) | defp deps do
method halt_on_warnings? (line 37) | defp halt_on_warnings?(:test), do: false
method halt_on_warnings? (line 38) | defp halt_on_warnings?(_), do: true
FILE: test/connection_test.exs
class RakNet.ConnectionTest (line 1) | defmodule RakNet.ConnectionTest
FILE: test/message_test.exs
class RakNet.MessageTest (line 1) | defmodule RakNet.MessageTest
FILE: test/packet_test.exs
class RakNet.PacketTest (line 1) | defmodule RakNet.PacketTest
FILE: test/server_test.exs
class RakNet.ServerTest (line 1) | defmodule RakNet.ServerTest
method start_server (line 398) | defp start_server(port, timeout_ms \\ 10_000, client_module \\ nil) do
method start_xplane_server (line 416) | defp start_xplane_server(port) do
method make_client_send_fns (line 433) | defp make_client_send_fns(client_port, server_host_and_port, ip_versio...
method packet_decode (line 451) | defp packet_decode(packet_hex_string, pad_to_byte_length \\ 0) do
method server_sent_packet_with (line 461) | defp server_sent_packet_with(packet_spec, has_embedded_timestamp \\ fa...
method parse_packet_type (line 481) | defp parse_packet_type(<<type::size(8), remainder::binary>>, has_embed...
method drop_leading_bits (line 570) | defp drop_leading_bits(data, num_bits) do
method open_conn_req_1_with_retries (line 576) | defp open_conn_req_1_with_retries(client_send_padded, success_check, m...
method open_conn_req_1_with_retries (line 587) | defp open_conn_req_1_with_retries(_client_send_padded, _success_check,...
method receive_business_logic_msg (line 602) | def receive_business_logic_msg do
method receive_business_logic_ack (line 611) | def receive_business_logic_ack do
class AckRequestingClient (line 189) | defmodule AckRequestingClient
method new (line 193) | def new(connection_pid, test_pid) do
method bug_the_user (line 203) | def bug_the_user(connection_pid) do
class DummyClient (line 382) | defmodule DummyClient
method accept_data (line 385) | def accept_data(client, packet_type, buffer) do
FILE: test/system_address_test.exs
class RakNet.SystemAddressTest (line 1) | defmodule RakNet.SystemAddressTest
Condensed preview — 26 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (122K chars).
[
{
"path": ".circleci/config.yml",
"chars": 1829,
"preview": "# Elixir CircleCI 2.0 configuration file\n#\n# Check https://circleci.com/docs/2.0/language-elixir/ for more details\nversi"
},
{
"path": ".formatter.exs",
"chars": 94,
"preview": "[\n line_length: 140,\n inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*.{ex,exs}\"]\n]\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 767,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 595,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
},
{
"path": ".gitignore",
"chars": 578,
"preview": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up "
},
{
"path": ".idea/runConfigurations/Format.xml",
"chars": 502,
"preview": "<component name=\"ProjectRunConfigurationManager\">\n <configuration default=\"false\" name=\"Format\" type=\"MixRunConfigurati"
},
{
"path": ".idea/runConfigurations/Test.xml",
"chars": 340,
"preview": "<component name=\"ProjectRunConfigurationManager\">\n <configuration default=\"false\" name=\"Test\" type=\"MixExUnitRunConfigu"
},
{
"path": "LICENSE",
"chars": 1064,
"preview": "MIT License\n\nCopyright (c) 2021 X-Plane\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof"
},
{
"path": "README.md",
"chars": 6454,
"preview": "# Elixir RakNet\n\n**`main` build status**: [\n def receive(client, packet_type, pa"
},
{
"path": "lib/message.ex",
"chars": 3230,
"preview": "defmodule RakNet.Message do\n @moduledoc \"\"\"\n Message types that RakNet can send. These are the first 8 bits of the pac"
},
{
"path": "lib/packet.ex",
"chars": 4872,
"preview": "defmodule RakNet.Packet do\n @moduledoc \"Encoding & decoding utils for RakLib data, taken from ExRakLib\"\n require Logge"
},
{
"path": "lib/reliability_layer.ex",
"chars": 3613,
"preview": "defmodule RakNet.ReliabilityLayer.Reliability do\n @moduledoc \"Taken from RakNet's PacketPriority.h\"\n\n @names_and_vals "
},
{
"path": "lib/server.ex",
"chars": 8588,
"preview": "defmodule RakNet.Server do\n @moduledoc \"\"\"\n A server implementing the RakNet protocol\n \"\"\"\n require Logger\n import "
},
{
"path": "lib/system_address.ex",
"chars": 2558,
"preview": "defmodule RakNet.SystemAddress do\n @moduledoc \"Tools for encoding & decoding IP addresses & ports\"\n\n def encode(%{vers"
},
{
"path": "mix.exs",
"chars": 1046,
"preview": "defmodule RakNet.MixProject do\n use Mix.Project\n\n def project do\n [\n app: :raknet,\n version: \"0.1.0\",\n "
},
{
"path": "raknet.iml",
"chars": 3150,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"ELIXIR_MODULE\" version=\"4\">\n <component name=\"NewModuleRootManager"
},
{
"path": "test/connection_test.exs",
"chars": 1535,
"preview": "defmodule RakNet.ConnectionTest do\n use ExUnit.Case, async: true\n doctest RakNet.Connection\n import RakNet.Connection"
},
{
"path": "test/message_test.exs",
"chars": 1211,
"preview": "defmodule RakNet.MessageTest do\n use ExUnit.Case, async: true\n\n test \"lists the message name atoms\" do\n known_msg_n"
},
{
"path": "test/packet_test.exs",
"chars": 679,
"preview": "defmodule RakNet.PacketTest do\n use ExUnit.Case\n\n @test_data_packet <<25, 62, 69, 23, 124, 1, 0, 32, 0, 224, 105, 1, 0"
},
{
"path": "test/server_test.exs",
"chars": 29332,
"preview": "defmodule RakNet.ServerTest do\n use ExUnit.Case\n require Logger\n import RakNet.Packet, only: [decode_with_timestamp: "
},
{
"path": "test/system_address_test.exs",
"chars": 2330,
"preview": "defmodule RakNet.SystemAddressTest do\n use ExUnit.Case, async: true\n import Assertions, only: [assert_lists_equal: 2]\n"
},
{
"path": "test/test_helper.exs",
"chars": 15,
"preview": "ExUnit.start()\n"
}
]
About this extraction
This page contains the full source code of the X-Plane/elixir-raknet GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 26 files (114.2 KB), approximately 32.4k tokens, and a symbol index with 129 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.