Compare commits

..

6 Commits

Author SHA1 Message Date
a30b9f1a62 Rewrote timeout system 2020-03-30 00:59:53 +02:00
e9ef282c95 Keep alive system 2020-03-30 00:38:02 +02:00
f55275a627 Preparement for rcon support 2020-03-29 23:43:08 +02:00
c1ad95d233 Query protocol 2020-03-29 23:05:00 +02:00
0c0a8d327c Rename imports 2020-03-29 19:12:31 +02:00
ee08f6a3b1 Pass server config to clients 2020-03-29 19:04:20 +02:00
11 changed files with 187 additions and 28 deletions

3
.gitignore vendored
View File

@ -96,3 +96,6 @@ ENV/
# Logs # Logs
logs/ logs/
# vscode
.vscode/

View File

@ -14,6 +14,7 @@
"port": 7777, "port": 7777,
"hostname": "Python > C", "hostname": "Python > C",
"password": "", "password": "",
"rcon_password": "changeme",
"max_players": 50, "max_players": 50,
"mode": "debug", "mode": "debug",
"language": "python" "language": "python"

View File

@ -1,12 +1,12 @@
from sampy.env import Environment from sampy.env import Environment
from sampy.struct.server import Server from sampy.struct.server import ServerConfig
from sampy.shared.glob import config from sampy.shared.glob import config
environments = [] environments = []
for server in config["demo"]["servers"]: for server in config["demo"]["servers"]:
server_config = Server(**server) # Initialize a new Server struct every time even if you are just changing the port (required due to reference and automation values) server_config = ServerConfig(**server) # Initialize a new Server struct every time even if you are just changing the port (required due to reference and automation values)
env = Environment(server_config) env = Environment(server_config)
environments.append(env) environments.append(env)

View File

@ -1,5 +1,7 @@
import struct import struct
import socket import socket
import asyncio
from time import time
from . import base from . import base
from . import query from . import query
@ -9,19 +11,28 @@ STATE_UNKNOWN = (0, base.BaseClient)
STATE_QUERY = (1, query.QueryClient) STATE_QUERY = (1, query.QueryClient)
STATE_PLAYER = (2, player.PlayerClient) STATE_PLAYER = (2, player.PlayerClient)
TIMEOUT = 10 # assume connection is closed after 10 seconds of inactivity (change this to a higher value so you dont timeout while debugging might be a good idea)
class Client: class Client:
def __init__(self, socket: socket.socket, ip: str, port: int): def __init__(self, server: "__ServerInstance__", ip: str, port: int):
self.socket = socket self.server = server
self.ip = ip self.ip = ip
self.port = port self.port = port
self.set_state(STATE_UNKNOWN) self.set_state(STATE_UNKNOWN)
self.last_active = time()
self.keep_alive_task = asyncio.create_task( self.keep_alive() )
self.connected = True # keep_alive will set this to False if connection has not been interacted with for a while (allowing server loop to remove their reference)
def set_state(self, state: tuple): def set_state(self, state: tuple):
#self.keep_alive_task.cancel()
self.state = state self.state = state
self.client = self.state[1](self.socket, self.ip, self.port) self.client = self.state[1](self.server, self.ip, self.port)
async def on_packet(self, packet: bytes): async def on_packet(self, packet: bytes):
self.last_active = time()
if self.state == STATE_UNKNOWN: if self.state == STATE_UNKNOWN:
# We are currently unaware if this is a player client or query client, but we got a packet that will be our check to know # We are currently unaware if this is a player client or query client, but we got a packet that will be our check to know
if packet.startswith(b"SAMP"): if packet.startswith(b"SAMP"):
@ -30,3 +41,12 @@ class Client:
self.set_state(STATE_PLAYER) self.set_state(STATE_PLAYER)
await self.client.on_packet(packet) await self.client.on_packet(packet)
async def keep_alive(self): # Maybe bad name for this method as it rather checks if connection is dropped
while True:
timestamp = time()
if self.last_active + TIMEOUT - timestamp < 0:
self.connected = False
return
await asyncio.sleep(self.last_active + TIMEOUT - timestamp)

View File

@ -5,8 +5,8 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BaseClient: class BaseClient:
def __init__(self, socket: socket.socket, ip: str, port: int): def __init__(self, server: "__ServerInstance__", ip: str, port: int):
self.socket = socket self.server = server
self.ip = ip self.ip = ip
self.port = port self.port = port
@ -14,3 +14,7 @@ class BaseClient:
async def on_packet(self, packet: bytes): async def on_packet(self, packet: bytes):
logger.debug("on_packet(%s)" % packet) logger.debug("on_packet(%s)" % packet)
async def send(self, packet: bytes):
sock: socket.socket = self.server.socket
sock.sendto(packet, (self.ip, self.port))

View File

@ -1,13 +1,14 @@
import socket import socket
from . import base from .base import BaseClient
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class PlayerClient(base.BaseClient): class PlayerClient(BaseClient):
def __init__(self, socket: socket.socket, ip: str, port: int): def __init__(self, server: "__ServerInstance__", ip: str, port: int):
super().__init__(socket, ip, port) super().__init__(server, ip, port)
logger.debug("Client resolved to PlayerClient")
async def on_packet(self, packet: bytes): async def on_packet(self, packet: bytes):
logger.debug("on_packet(%s)" % packet) logger.debug("on_packet(%s)" % packet)

View File

@ -1,13 +1,118 @@
import socket import socket
import struct
from . import base from .base import BaseClient
from ..shared import glob
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class QueryClient(base.BaseClient): class QueryClient(BaseClient):
def __init__(self, socket: socket.socket, ip: str, port: int): def __init__(self, server: "__ServerInstance__", ip: str, port: int):
super().__init__(socket, ip, port) super().__init__(server, ip, port)
logger.debug("Client resolved to QueryClient")
self.handlers = {
b"i": self.query_i,
b"r": self.query_r,
b"c": self.query_c,
b"d": self.query_d,
b"p": self.query_p,
b"x": self.query_x
}
async def on_packet(self, packet: bytes): async def on_packet(self, packet: bytes):
logger.debug("on_packet(%s)" % packet) logger.debug("on_packet(%s)" % packet)
if len(packet) <= 10: # Invalid
return
if packet[10:11] in self.handlers:
packet = await self.handlers[packet[10:11]](packet)
if len(packet): # Send packet back if not 0
await self.send(packet)
async def query_i(self, packet: bytes) -> bytes:
len_hostname = len(self.server.config.hostname)
len_mode = len(self.server.config.mode)
len_language = len(self.server.config.language)
return packet + struct.pack(b"<?HHI%dsI%dsI%ds" % (len_hostname, len_mode, len_language),
len(self.server.config.password) != 0,
len(await self.server.get_online_players()),
self.server.config.max_players,
len_hostname,
self.server.config.hostname.encode(),
len_mode,
self.server.config.mode.encode(),
len_language,
self.server.config.language.encode()
)
async def query_r(self, packet: bytes) -> bytes:
data = []
rules = await self.server.get_rules()
data.append(len(rules))
for k, v in rules.items():
data += [
len(k), k,
len(v), v
]
return packet + struct.pack(b"<H" + (b"B%dsB%ds" * data[0]) % tuple(len(y) for x in rules.items() for y in x), *data)
async def query_c(self, packet: bytes) -> bytes:
data = []
scores = await self.server.get_players_scores()
data.append(len(scores))
for k, v in scores.items():
data += [
len(k), k,
v
]
return packet + struct.pack(b"<H" + (b"B%dsI" * data[0]) % tuple(len(x) for x in scores.keys()), *data)
async def query_d(self, packet: bytes) -> bytes:
data = []
players = await self.server.get_online_players()
data.append(len(players))
for p in players:
data += [
p["id"],
len(p["nick"]), p["nick"],
p["score"],
p["ping"]
]
return packet + struct.pack(b"<H" + (b"cc%dsII" * data[0]) % tuple(len(p["nick"]) for p in players), *data)
async def query_p(self, packet: bytes) -> bytes:
return packet
async def query_x(self, packet: bytes) -> bytes:
len_pswd, = struct.unpack_from(b"<H", packet, 11)
pswd, len_cmd = struct.unpack_from(b"<%dsH" % len_pswd, packet, 13)
cmd = struct.unpack_from(b"<%ds" % len_cmd, packet, 15 + len_pswd)
if len(self.server.config.rcon_password) == 0:
msg = b"Remote Console is not enabled on this server."
elif self.server.config.rcon_password.encode() != pswd:
msg = b"Invalid RCON password."
logger.warning("BAD RCON ATTEMPT BY: %s:%d" % (self.ip, self.port))
else:
# TODO: Add rcon client to command stdouts
return b"" # No response as all is ok
return packet[:11] + struct.pack(b"<H%ds" % len(msg), len(msg), msg)

View File

@ -1,21 +1,20 @@
import asyncio import asyncio
from threading import Thread from threading import Thread
from .struct.server import Server as struct from .struct.server import ServerConfig
from . import server from .server import Server
class Environment(Thread): class Environment(Thread):
def __init__(self, config: struct): def __init__(self, config: ServerConfig):
super().__init__() super().__init__()
self.daemon = True self.daemon = True
self.event_loop = asyncio.get_event_loop() self.event_loop = asyncio.get_event_loop()
self.config = config self.config = config
self.server = server.Server(self.config) self.server = Server(self.config)
def command(self, cmd: str): def command(self, cmd: str):
self.event_loop.create_task(self.server.on_command(cmd)) self.event_loop.create_task(self.server.on_command(cmd))
def run(self): def run(self):
self.event_loop.run_until_complete(self.server.main()) self.event_loop.run_until_complete(self.server.main())
print("Ended?")

View File

@ -2,16 +2,17 @@ import socket
import asyncio import asyncio
from select import select # This is straight up magic from select import select # This is straight up magic
from .struct.server import Server as struct from .struct.server import ServerConfig
from .client import Client from .client import Client
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Server: class Server:
def __init__(self, config: struct): def __init__(self, config: ServerConfig):
self.config = config self.config = config
self.clients = {} self.clients = {}
self.rcon_clients = {}
async def create_socket(self): async def create_socket(self):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
@ -22,6 +23,18 @@ class Server:
async def on_command(self, cmd: str): async def on_command(self, cmd: str):
logger.debug("on_command(%s)" % cmd) logger.debug("on_command(%s)" % cmd)
# TODO: When commands return a reponse we also want to forward this to potential rcon clients
async def get_online_players(self): # TODO: Get data from server's client objects
return [
{"nick": b"Sunpy", "score": 64, "ping": 8, "id": 1} # replace id with function to get player's id
]
async def get_rules(self): # TODO
return {b"Rule name sample": b"Rule value", b"weburl": b"https://git.osufx.com/Sunpy/sampy"}
async def get_players_scores(self): # TODO
return {b"Sunpy": 64, b"username": 123}
async def main(self): async def main(self):
await self.create_socket() await self.create_socket()
@ -34,7 +47,14 @@ class Server:
if addr not in self.clients: if addr not in self.clients:
ip, port = addr ip, port = addr
self.clients[addr] = Client(self.socket, ip, port) self.clients[addr] = Client(self, ip, port)
await self.clients[addr].on_packet(data) await self.clients[addr].on_packet(data)
await asyncio.sleep(0) disconnected = [c for c in self.clients.values() if c.connected == False]
for c in disconnected: # Remove dead connections
addr = (c.ip, c.port)
if addr in self.clients:
del self.clients[addr]
logger.debug("free(%s)" % c)
await asyncio.sleep(0)

View File

@ -10,8 +10,11 @@ if not os.path.isfile("config.json"):
with open("config.json", "r") as f: with open("config.json", "r") as f:
config = json.load(f) config = json.load(f)
# aliases
conf = config["sampy"]
conf_log = conf["logging"]
# Setup logger # Setup logger
conf_log = config["sampy"]["logging"] # alias
## fix for logging level ## fix for logging level
default_logging_fallback = False default_logging_fallback = False
if type(conf_log["level"]) is not int: if type(conf_log["level"]) is not int:

View File

@ -1,9 +1,10 @@
from random import randint from random import randint
class Server: class ServerConfig:
def __init__(self, def __init__(self,
host: str, port: int, host: str, port: int,
hostname: str, password: str, hostname: str, password: str,
rcon_password: str,
max_players: int, max_players: int,
mode: str, language: str): mode: str, language: str):
self.host = host self.host = host
@ -12,6 +13,8 @@ class Server:
self.hostname = hostname self.hostname = hostname
self.password = password self.password = password
self.rcon_password = rcon_password
self.max_players = max_players self.max_players = max_players
self.mode = mode self.mode = mode