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/
|
||||
|
||||
# vscode
|
||||
.vscode/
|
|
@ -14,6 +14,7 @@
|
|||
"port": 7777,
|
||||
"hostname": "Python > C",
|
||||
"password": "",
|
||||
"rcon_password": "changeme",
|
||||
"max_players": 50,
|
||||
"mode": "debug",
|
||||
"language": "python"
|
||||
|
|
4
main.py
4
main.py
|
@ -1,12 +1,12 @@
|
|||
from sampy.env import Environment
|
||||
from sampy.struct.server import Server
|
||||
from sampy.struct.server import ServerConfig
|
||||
|
||||
from sampy.shared.glob import config
|
||||
|
||||
environments = []
|
||||
|
||||
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)
|
||||
environments.append(env)
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import struct
|
||||
import socket
|
||||
import asyncio
|
||||
from time import time
|
||||
|
||||
from . import base
|
||||
from . import query
|
||||
|
@ -9,19 +11,28 @@ STATE_UNKNOWN = (0, base.BaseClient)
|
|||
STATE_QUERY = (1, query.QueryClient)
|
||||
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:
|
||||
def __init__(self, socket: socket.socket, ip: str, port: int):
|
||||
self.socket = socket
|
||||
def __init__(self, server: "__ServerInstance__", ip: str, port: int):
|
||||
self.server = server
|
||||
self.ip = ip
|
||||
self.port = port
|
||||
|
||||
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):
|
||||
#self.keep_alive_task.cancel()
|
||||
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):
|
||||
self.last_active = time()
|
||||
|
||||
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
|
||||
if packet.startswith(b"SAMP"):
|
||||
|
@ -30,3 +41,12 @@ class Client:
|
|||
self.set_state(STATE_PLAYER)
|
||||
|
||||
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__)
|
||||
|
||||
class BaseClient:
|
||||
def __init__(self, socket: socket.socket, ip: str, port: int):
|
||||
self.socket = socket
|
||||
def __init__(self, server: "__ServerInstance__", ip: str, port: int):
|
||||
self.server = server
|
||||
self.ip = ip
|
||||
self.port = port
|
||||
|
||||
|
@ -14,3 +14,7 @@ class BaseClient:
|
|||
|
||||
async def on_packet(self, packet: bytes):
|
||||
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
|
||||
|
||||
from . import base
|
||||
from .base import BaseClient
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PlayerClient(base.BaseClient):
|
||||
def __init__(self, socket: socket.socket, ip: str, port: int):
|
||||
super().__init__(socket, ip, port)
|
||||
class PlayerClient(BaseClient):
|
||||
def __init__(self, server: "__ServerInstance__", ip: str, port: int):
|
||||
super().__init__(server, ip, port)
|
||||
logger.debug("Client resolved to PlayerClient")
|
||||
|
||||
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 struct
|
||||
|
||||
from . import base
|
||||
from .base import BaseClient
|
||||
from ..shared import glob
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class QueryClient(base.BaseClient):
|
||||
def __init__(self, socket: socket.socket, ip: str, port: int):
|
||||
super().__init__(socket, ip, port)
|
||||
class QueryClient(BaseClient):
|
||||
def __init__(self, server: "__ServerInstance__", ip: str, port: int):
|
||||
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):
|
||||
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
|
||||
from threading import Thread
|
||||
|
||||
from .struct.server import Server as struct
|
||||
from . import server
|
||||
from .struct.server import ServerConfig
|
||||
from .server import Server
|
||||
|
||||
class Environment(Thread):
|
||||
def __init__(self, config: struct):
|
||||
def __init__(self, config: ServerConfig):
|
||||
super().__init__()
|
||||
self.daemon = True
|
||||
self.event_loop = asyncio.get_event_loop()
|
||||
|
||||
self.config = config
|
||||
self.server = server.Server(self.config)
|
||||
self.server = Server(self.config)
|
||||
|
||||
def command(self, cmd: str):
|
||||
self.event_loop.create_task(self.server.on_command(cmd))
|
||||
|
||||
def run(self):
|
||||
self.event_loop.run_until_complete(self.server.main())
|
||||
print("Ended?")
|
|
@ -2,16 +2,17 @@ import socket
|
|||
import asyncio
|
||||
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
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Server:
|
||||
def __init__(self, config: struct):
|
||||
def __init__(self, config: ServerConfig):
|
||||
self.config = config
|
||||
self.clients = {}
|
||||
self.rcon_clients = {}
|
||||
|
||||
async def create_socket(self):
|
||||
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):
|
||||
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):
|
||||
await self.create_socket()
|
||||
|
@ -34,7 +47,14 @@ class Server:
|
|||
|
||||
if addr not in self.clients:
|
||||
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 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)
|
||||
|
|
|
@ -10,8 +10,11 @@ if not os.path.isfile("config.json"):
|
|||
with open("config.json", "r") as f:
|
||||
config = json.load(f)
|
||||
|
||||
# aliases
|
||||
conf = config["sampy"]
|
||||
conf_log = conf["logging"]
|
||||
|
||||
# Setup logger
|
||||
conf_log = config["sampy"]["logging"] # alias
|
||||
## fix for logging level
|
||||
default_logging_fallback = False
|
||||
if type(conf_log["level"]) is not int:
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from random import randint
|
||||
|
||||
class Server:
|
||||
class ServerConfig:
|
||||
def __init__(self,
|
||||
host: str, port: int,
|
||||
hostname: str, password: str,
|
||||
rcon_password: str,
|
||||
max_players: int,
|
||||
mode: str, language: str):
|
||||
self.host = host
|
||||
|
@ -12,6 +13,8 @@ class Server:
|
|||
self.hostname = hostname
|
||||
self.password = password
|
||||
|
||||
self.rcon_password = rcon_password
|
||||
|
||||
self.max_players = max_players
|
||||
|
||||
self.mode = mode
|
||||
|
|
Loading…
Reference in New Issue
Block a user