Custom Communication Port (CCP)

Custom Packages that plugins can require for common usages.
User avatar
iPlayer
Developer
Posts: 590
Joined: Sat Nov 14, 2015 8:37 am
Location: Moscow
Contact:

Custom Communication Port (CCP)

Postby iPlayer » Sat Sep 24, 2016 10:53 pm

Custom Communication Port (CCP) Package
by Kirill "iPlayer" Mysnik

GitHub repo: KirillMysnik/SP-CCP


What's this?
This package opens one extra TCP port on your Source Dedicated Server - that will allow plugins to listen for incoming connections and communicate with external applications.
GitHub repo includes 3 directories: examples, srcds and external.
examples folder contains examples / sample scripts (both plugins and external scripts);
srcds folder contains custom package for Source.Python;
external folder contains almost the same package but for external applications (e.g. no AutoUnload usage, GameThread is replaced with Thread etc).

Examples
Examples will be stored at GitHub repo together with the package: https://github.com/KirillMysnik/SP-CCP/tree/master/examples
Let's take a look a the Admin Chat example. It contains Source.Python plugin and external Python 3.5 script. The main purpose of the script is to receive a text input from console and then send this text to the game chat:
Image
Image

Admin Chat (server plugin)

Syntax: Select all

from colors import RED, WHITE
from messages import SayText2

from ccp.receive import RequestBasedReceiver


@RequestBasedReceiver('ccp_admin_chat')
def ccp_admin_chat_receiver(addr, data):
SayText2("{color1}ADMIN: {color2}{message}".format(
color1=RED,
color2=WHITE,
message=data.decode('utf-8')
)).send()
return "OK"


Admin Chat (external script)

Syntax: Select all

#!python3
from ccp.constants import CommunicationMode
from ccp.transmit import SRCDSClient


class TestPluginClient(SRCDSClient):
def on_connected(self):
print("Connection established, setting mode to REQUEST-BASED...")
self.set_mode(CommunicationMode.REQUEST_BASED)

def on_comm_accepted(self):
self.prompt_message()

def on_data_received(self, data):
if data == b"OK":
print("Message was delivered successfully!")
else:
print("Failed to deliver the message")

self.prompt_message()

def on_comm_error(self):
print("Something went wrong on the other side...")

def on_protocol_error(self):
print("There was a misunderstanding between CCP package we use and "
"CCP package SRCDS uses")

def on_comm_end(self):
print("Communication has ended without errors.")

def on_nobody_home(self):
print("Receiving plugin has been unloaded (or was not loaded at all)")

def prompt_message(self):
message = input("Enter the message (leave empty to quit): ")
if not message:
self.stop()

else:
self.send_data(message.encode('utf-8'))


test_plugin_client = TestPluginClient(('127.0.0.1', 28080), 'ccp_admin_chat')
test_plugin_client.start()



Installation
SRCDS
1. Install latest Source.Python
3. Copy contents of the srcds folder from the latest CCP release archive to your mod directory (e.g. "cstrike").
4. Go to addons/source-python/data/custom/ccp/config.ini. It should look something like this:

Code: Select all

[server]
host=
port=28080
whitelist=127.0.0.1,localhost

port option is custom communication port number of the game server;
whitelist option is a comma-separated list of the hosts that are allowed to connect to the custom communication port of the game server.

API

ccp.receive.RequestBasedReceiver
It's a decorator that you use the following way:

Syntax: Select all

@RequestBasedReceiver("my_plugin")
def my_plugin_receiver(addr, data):
...

Using the code above, my_plugin_receiver will be called every time some external application sends something to your plugin using "request-based" communication. Note that it can be called multiple times during one connection or connection may be closed after every call - you won't see the difference. The callback receives two positional arguments: first one is the address of the external application (a pair (host, port) where host is a string representing either a hostname or an IPv4 address, and port is an integer) and the second one is the data the application has sent to your plugin. Type of data is always bytes.
The callback should always return either str or bytes value which then will be sent back to the external application. If callback returns str value, it's encoded into bytes using UTF-8.
If the callback fails to return a proper value or raises an exception, connection to the external app will be terminated.


ccp.receive.RawReceiver
It's a class you should subclass the following way:

Syntax: Select all

class MyPluginReceiver(RawReceiver):
plugin_name = "my_plugin"

def on_data_received(self, data):
...

def on_connection_abort(self):
...

Using RawReceiver is a more flexible approach. An instance of MyPluginReceiver will be automatically created every time some external application establishes a connection and sets its mode to "raw" communication.
With the default constructor ommited, every MyPluginReceiver instance will have an addr attribute and two methods: send_data and stop.

send_data(data) accepts only one positional argument and it should be the data to send to the external application - either bytes or str.

stop() doesn't accept any arguments and just stops the communication with the external application.

The MyPluginReceiver instance may also override two other methods: on_data_received and on_connection_abort.

on_data_received(data) will be called with one positional argument - the data that external application has sent to your plugin. Type of data is always bytes.

on_connection_abort() will be called when connection is unexpectedly closed or aborted.


ccp.transmit.SRCDSClient
It's a class you use to connect to the plugin on the game server. I haven't written a documentation to its API yet, but you can use Admin Chat external script as an example. Also look through these lines: https://github.com/KirillMysnik/SP-CCP/blob/master/srcds/addons/source-python/packages/custom/ccp/transmit.py#L78-L127


Summary
Main reason of writing this package: currently MOTDPlayer package binds its own TCP-port just for its needs. If I make MOTDPlayer depend on CCP, that'll allow to share this extra TCP-port (as you obviously don't want every plugin to open its extra port).

As for now, using this package allows you to:
  • establish a connection from one game server to another and exchange data
  • establish a connection from an external application to the game server and exchange data
It does NOT allow you to:
  • establish a connection from the game server to an external application
But I plan on including a receive module into external part of the package, too, so that the game server will be able to establish connections to the external application by itself.

As for now, it's possible to create a thousand connections from an external application to the game server and just abandon them - so be careful what hosts you whitelist.
As for now, RawReceiver class is completely untested.


GitHub repo: KirillMysnik/SP-CCP

Thanks.
Image /id/its_iPlayer
My plugins: Map Cycle • Killstreaker • DeadChat • Infinite Jumping • TripMines • AdPurge • Bot Damage • PLRBots • Entity AntiSpam

Hail, Companion. [...] Hands to yourself, sneak thief. Image
User avatar
Kill
Member
Posts: 88
Joined: Wed Aug 31, 2016 10:05 pm

Re: Custom Communication Port (CCP)

Postby Kill » Sat Sep 24, 2016 11:18 pm

This is amazing, well done iPlayer!

Return to “Custom Packages”

Who is online

Users browsing this forum: No registered users and 13 guests