Compare commits
6 Commits
fee2005eef
...
a30b9f1a62
Author | SHA1 | Date | |
---|---|---|---|
a30b9f1a62 | |||
e9ef282c95 | |||
f55275a627 | |||
c1ad95d233 | |||
0c0a8d327c | |||
ee08f6a3b1 |
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -96,3 +96,6 @@ ENV/
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs/
|
logs/
|
||||||
|
|
||||||
|
# vscode
|
||||||
|
.vscode/
|
|
@ -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"
|
||||||
|
|
4
main.py
4
main.py
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
@ -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))
|
||||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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?")
|
|
|
@ -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)
|
||||||
|
|
||||||
|
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)
|
await asyncio.sleep(0)
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user