Skip to content

Instantly share code, notes, and snippets.

@evadne
Created November 12, 2018 01:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save evadne/5d4546eea52d2634f2b4d9386f43f2ab to your computer and use it in GitHub Desktop.
Save evadne/5d4546eea52d2634f2b4d9386f43f2ab to your computer and use it in GitHub Desktop.
Source RCON client in Elixir (using gen_tcp)
defmodule HuddleGateway.External.SourceRemoteControl do
@moduledoc """
A simple gen_tcp based implementation of a Source RCON client
- [Source RCON Protocol](https://developer.valvesoftware.com/wiki/Source_RCON_Protocol)
By calling `connect/2`, you can obtain an open TCP socket,
which can then be used with `auth/3` or `exec/3`.
"""
@connect_timeout 5000
@receive_timeout 5000
@spec connect(binary(), non_neg_integer()) :: {:ok, :gen_tcp.socket} | {:error, any()}
@spec auth(:gen_tcp.socket, non_neg_integer(), String.t) :: {:ok, non_neg_integer()} | {:error, any()}
@spec exec(:gen_tcp.socket, non_neg_integer(), String.t) :: {:ok, non_neg_integer(), binary()} | {:error, any()}
@doc """
Open a connection to the nominated server. Use raw mode, so the amount of data
to be read can be controlled precisely.
"""
def connect(host, port) do
socket_host = to_charlist(host)
socket_options = [active: false, packet: :raw]
:gen_tcp.connect(socket_host, port, socket_options, @connect_timeout)
end
@doc """
Authenticate with a given RCON password.
"""
def auth(socket, from_sequence, password) do
with \
auth_sequence <- from_sequence + 1,
auth_packet <- build_packet(auth_sequence, :auth, password),
:ok <- send_packet(socket, auth_packet),
{:ok, {auth_sequence, :exec_response, ""}} <- receive_packet(socket),
{:ok, {auth_sequence, :auth_response, ""}} <- receive_packet(socket)
do
{:ok, auth_sequence}
else
{:error, :closed} -> {:error, :unauthorised}
{:error, reason} -> {:error, reason}
end
end
@doc """
Run a RCON command remotely and wait for resopnse. This will send 2 packets,
so Source RCON commands that have large repsonses can be handled properly. See
the Source Developer page for details.
"""
def exec(socket, from_sequence, command) do
with \
exec_sequence <- from_sequence + 1,
over_sequence <- exec_sequence + 1,
exec_packet <- build_packet(exec_sequence, :exec, command),
over_packet <- build_packet(over_sequence, :over),
:ok <- send_packet(socket, exec_packet),
:ok <- send_packet(socket, over_packet),
{:ok, response} <- drain_packets(socket, over_sequence)
do
{:ok, over_sequence, response}
end
end
defp drain_packets(socket, over_sequence, response \\ <<>>) do
case receive_packet(socket) do
{:ok, {^over_sequence, :exec_response, _}} ->
{:ok, response}
{:ok, {_, :exec_response, partial_response}} ->
drain_packets(socket, over_sequence, response <> partial_response)
end
end
defp outgoing_packet_type_for(:auth), do: 3
defp outgoing_packet_type_for(:exec), do: 2
defp outgoing_packet_type_for(:over), do: -1 # to allow multiple-packet responses
defp incoming_packet_type_for(2), do: :auth_response
defp incoming_packet_type_for(0), do: :exec_response
defp build_packet(sequence, type, body \\ "") do
request_length = byte_size(body) + 10
encoded_type = outgoing_packet_type_for(type)
request_head = <<
request_length :: little-integer-signed-size(32),
sequence :: little-integer-signed-size(32),
encoded_type :: little-integer-signed-size(32)
>>
request_tail = <<0, 0>>
[request_head, body, request_tail]
end
defp send_packet(socket, packet) do
:gen_tcp.send(socket, packet)
end
defp receive_packet(socket, timeout \\ @receive_timeout) do
with \
{:ok, response_head} <- :gen_tcp.recv(socket, 4, timeout),
{:ok, response_size} <- parse_response_head(binary_from_response(response_head)),
{:ok, response_rest} <- :gen_tcp.recv(socket, response_size, timeout),
{:ok, response} <- parse_response_rest(response_size - 10, binary_from_response(response_rest))
do
{:ok, response}
else
{:error, :closed} = x -> x
_ -> {:error, :invalid_response}
end
end
defp parse_response_head(<<size :: little-integer-signed-size(32)>>) do
{:ok, size}
end
defp parse_response_head(_) do
{:error, :badarg}
end
defp parse_response_rest(body_size, data) do
case data do
<<
response_sequence :: little-integer-signed-size(32),
response_type :: little-integer-signed-size(32),
response_body :: binary-size(body_size),
0, 0
>> ->
decoded_type = incoming_packet_type_for(response_type)
{:ok, {response_sequence, decoded_type, response_body}}
_ ->
{:error, :badarg}
end
end
defp binary_from_response(response) when is_binary(response) do
response
end
defp binary_from_response(response) when is_list(response) do
:erlang.iolist_to_binary(response)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment