diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index a0139cf..68588e8 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -28,4 +28,7 @@ jobs: ./integration_test/run.sh chat - name: Test reconnect run: | - ./integration_test/run.sh reconnect \ No newline at end of file + ./integration_test/run.sh reconnect + - name: Test rcon + run: | + ./integration_test/run.sh rcon \ No newline at end of file diff --git a/examples/06_join_leave_messages.rb b/examples/06_join_leave_messages.rb index 49b6723..10d1006 100755 --- a/examples/06_join_leave_messages.rb +++ b/examples/06_join_leave_messages.rb @@ -20,5 +20,5 @@ Signal.trap('INT') do client.disconnect end -# connect and detach thread +# connect and block main thread client.connect('localhost', 8303, detach: false) diff --git a/examples/07_rcon.rb b/examples/07_rcon.rb new file mode 100755 index 0000000..09f8257 --- /dev/null +++ b/examples/07_rcon.rb @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative '../lib/teeworlds-client' + +client = TeeworldsClient.new + +client.on_connected do |_| + client.rcon_auth(password: '123') + client.rcon('shutdown') +end + +client.on_rcon_line do |ctx| + puts "[rcon] #{ctx.data[:line]}" +end + +# connect and block main thread +client.connect('localhost', 8303, detach: false) diff --git a/integration_test/rcon_shutdown.rb b/integration_test/rcon_shutdown.rb new file mode 100755 index 0000000..a8df950 --- /dev/null +++ b/integration_test/rcon_shutdown.rb @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative '../lib/teeworlds-client' + +client = TeeworldsClient.new + +client.on_connected do |_| + client.rcon_auth(password: 'rcon') + client.rcon('shutdown') +end + +client.on_rcon_line do |ctx| + puts "[rcon] #{ctx.data[:line]}" +end + +# connect and block main thread +client.connect('localhost', 8377, detach: false) diff --git a/integration_test/run.sh b/integration_test/run.sh index 9509b08..ee2f7ba 100755 --- a/integration_test/run.sh +++ b/integration_test/run.sh @@ -3,10 +3,12 @@ cd "$(dirname "$0")" || exit 1 twbin=teeworlds_srv +rubybin=send_chat_hello.rb +srvcfg='sv_rcon_password rcon;sv_port 8377;killme' function cleanup() { echo "[*] shutting down server ..." - pkill -f "$twbin sv_port 8377;killme" + pkill -f "$twbin $srvcfg" [[ "$_timout_pid" != "" ]] && kill "$_timout_pid" &> /dev/null } @@ -15,32 +17,47 @@ trap cleanup EXIT function timeout() { local seconds="$1" sleep "$seconds" - pkill -f 'send_chat_hello.rb' + pkill -f "$rubybin" + echo "killing: $rubybin" echo "Error: timeouted" } if [[ -x "$(command -v teeworlds_srv)" ]] then - teeworlds_srv "sv_port 8377;killme" &> server.txt & + teeworlds_srv "$srvcfg" &> server.txt & elif [[ -x "$(command -v teeworlds-server)" ]] then - teeworlds-server "sv_port 8377;killme" &> server.txt & + teeworlds-server "$srvcfg" &> server.txt & twbin='teeworlds-server' elif [[ -x "$(command -v teeworlds-srv)" ]] then - teeworlds-srv "sv_port 8377;killme" &> server.txt & + teeworlds-srv "$srvcfg" &> server.txt & twbin='teeworlds-srv' else echo "Error: please install a teeworlds_srv" exit 1 fi -timeout 3 killme & -_timout_pid=$! testname="${1:-chat}" - echo "[*] running test '$testname' ..." +if [ "$testname" == "chat" ] +then + rubybin=send_chat_hello.rb +elif [ "$testname" == "reconnect" ] +then + rubybin=reconnect.rb +elif [ "$testname" == "rcon" ] +then + rubybin=rcon_shutdown.rb +else + echo "Error: unkown test '$testname'" + exit 1 +fi + +timeout 3 killme & +_timout_pid=$! + function fail() { local msg="$1" tail client.txt @@ -51,7 +68,7 @@ function fail() { if [ "$testname" == "chat" ] then - ruby ./send_chat_hello.rb &> client.txt + ruby "$rubybin" &> client.txt if ! grep -q 'hello world' server.txt then @@ -59,16 +76,24 @@ then fi elif [ "$testname" == "reconnect" ] then - ruby ./reconnect.rb &> client.txt + ruby "$rubybin" &> client.txt if ! grep -q 'bar' server.txt then fail "Error: did not find 2nd chat message in server log" fi +elif [ "$testname" == "rcon" ] +then + ruby "$rubybin" &> client.txt + + if pgrep -f "$twbin $srvcfg" + then + fail "Error: server still running rcon shutdown failed" + fi else echo "Error: unkown test '$testname'" exit 1 fi -echo "[+] Test passed client sent chat message to server" +echo "[+] Test passed" diff --git a/lib/game_client.rb b/lib/game_client.rb index cdf1bea..12193ae 100644 --- a/lib/game_client.rb +++ b/lib/game_client.rb @@ -106,6 +106,15 @@ class GameClient @client.send_msg_startinfo end + def on_rcon_line(chunk) + u = Unpacker.new(chunk.data[1..]) + context = Context.new( + @client, + line: u.get_string + ) + @client.hooks[:rcon_line]&.call(context) + end + def on_emoticon(chunk); end def on_map_change(chunk) diff --git a/lib/teeworlds-client.rb b/lib/teeworlds-client.rb index a38bc46..1cd28a8 100755 --- a/lib/teeworlds-client.rb +++ b/lib/teeworlds-client.rb @@ -73,6 +73,10 @@ class TeeworldsClient @hooks[:connected] = block end + def on_rcon_line(&block) + @hooks[:rcon_line] = block + end + def send_chat(str) @netbase.send_packet( NetChunk.create_vital_header({ vital: true }, 4 + str.length) + @@ -170,6 +174,42 @@ class TeeworldsClient @netbase.send_packet(msg, 1) end + def rcon_auth(name, password = nil) + if name.instance_of?(Hash) + password = name[:password] + name = name[:name] + end + if password.nil? + raise "Error: password can not be empty\n" \ + " provide two strings: name, password\n" \ + " or a hash with the key :password\n" \ + "\n" \ + " rcon_auth('', '123')\n" \ + " rcon_auth(password: '123')\n" + end + data = [] + if name.nil? || name == '' + data += Packer.pack_str(password) + else # ddnet auth using name, password and some int? + data += Packer.pack_str(name) + data += Packer.pack_str(password) + data += Packer.pack_int(1) + end + msg = NetChunk.create_vital_header({ vital: true }, data.size + 1) + + [pack_msg_id(NETMSG_RCON_AUTH, system: true)] + + data + @netbase.send_packet(msg, 1) + end + + def rcon(command) + data = [] + data += Packer.pack_str(command) + msg = NetChunk.create_vital_header({ vital: true }, data.size + 1) + + [pack_msg_id(NETMSG_RCON_CMD, system: true)] + + data + @netbase.send_packet(msg, 1) + end + def send_msg_startinfo data = [] @@ -289,6 +329,8 @@ class TeeworldsClient @game_client.on_connected when NETMSG_NULL # should we be in alert here? + when NETMSG_RCON_LINE + @game_client.on_rcon_line(chunk) else puts "Unsupported system msg: #{chunk.msg}" exit(1)