Creating JSONRPC Server

This commit is contained in:
adator
2025-11-16 19:13:26 +01:00
parent 2e422c93e5
commit c371910066
11 changed files with 347 additions and 14 deletions

View File

@@ -14,6 +14,7 @@ REHASH_MODULES = [
'core.classes.modules.config', 'core.classes.modules.config',
'core.base', 'core.base',
'core.classes.modules.commands', 'core.classes.modules.commands',
'core.classes.modules.rpc',
'core.classes.interfaces.iprotocol', 'core.classes.interfaces.iprotocol',
'core.classes.interfaces.imodule', 'core.classes.interfaces.imodule',
'core.classes.protocols.command_handler', 'core.classes.protocols.command_handler',
@@ -69,6 +70,8 @@ def restart_service(uplink: 'Irc', reason: str = "Restarting with no reason!") -
def rehash_service(uplink: 'Irc', nickname: str) -> None: def rehash_service(uplink: 'Irc', nickname: str) -> None:
need_a_restart = ["SERVEUR_ID"] need_a_restart = ["SERVEUR_ID"]
uplink.Settings.set_cache('db_commands', uplink.Commands.DB_COMMANDS) uplink.Settings.set_cache('db_commands', uplink.Commands.DB_COMMANDS)
uplink.Loader.RpcServer.stop_server()
restart_flag = False restart_flag = False
config_model_bakcup = uplink.Config config_model_bakcup = uplink.Config
mods = REHASH_MODULES mods = REHASH_MODULES
@@ -80,7 +83,7 @@ def rehash_service(uplink: 'Irc', nickname: str) -> None:
channel=uplink.Config.SERVICE_CHANLOG channel=uplink.Config.SERVICE_CHANLOG
) )
uplink.Utils = sys.modules['core.utils'] uplink.Utils = sys.modules['core.utils']
uplink.Config = uplink.Loader.ConfModule.Configuration(uplink.Loader).configuration_model uplink.Config = uplink.Loader.Config = uplink.Loader.ConfModule.Configuration(uplink.Loader).configuration_model
uplink.Config.HSID = config_model_bakcup.HSID uplink.Config.HSID = config_model_bakcup.HSID
uplink.Config.DEFENDER_INIT = config_model_bakcup.DEFENDER_INIT uplink.Config.DEFENDER_INIT = config_model_bakcup.DEFENDER_INIT
uplink.Config.DEFENDER_RESTART = config_model_bakcup.DEFENDER_RESTART uplink.Config.DEFENDER_RESTART = config_model_bakcup.DEFENDER_RESTART
@@ -113,6 +116,8 @@ def rehash_service(uplink: 'Irc', nickname: str) -> None:
# Reload Main Commands Module # Reload Main Commands Module
uplink.Commands = uplink.Loader.CommandModule.Command(uplink.Loader) uplink.Commands = uplink.Loader.CommandModule.Command(uplink.Loader)
uplink.Loader.RpcServer = uplink.Loader.RpcServerModule.JSONRPCServer(uplink.Loader)
uplink.Loader.RpcServer.start_server()
uplink.Commands.DB_COMMANDS = uplink.Settings.get_cache('db_commands') uplink.Commands.DB_COMMANDS = uplink.Settings.get_cache('db_commands')
uplink.Loader.Base = uplink.Loader.BaseModule.Base(uplink.Loader) uplink.Loader.Base = uplink.Loader.BaseModule.Base(uplink.Loader)

View File

@@ -0,0 +1,240 @@
import base64
import json
import logging
from enum import Enum
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import TYPE_CHECKING, Any, Optional
from core.classes.modules.rpc.rpc_user import RPCUser
from core.classes.modules.rpc.rpc_channel import RPCChannel
from core.classes.modules.rpc.rpc_command import RPCCommand
if TYPE_CHECKING:
from core.loader import Loader
ProxyLoader: Optional['Loader'] = None
class RPCRequestHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
pass
def do_POST(self):
logs = ProxyLoader.Logs
self.server_version = 'Defender6'
self.sys_version = ProxyLoader.Config.CURRENT_VERSION
content_length = int(self.headers['Content-Length'])
body = self.rfile.read(content_length)
request_data: dict = json.loads(body)
rip, rport = self.client_address
if not self.authenticate(request_data):
return None
response_data = {
'jsonrpc': '2.0',
'id': request_data.get('id', 123)
}
method = request_data.get("method")
params: dict[str, Any] = request_data.get("params", {})
response_data['method'] = method
http_code = 200
match method:
case 'user.list':
user = RPCUser(ProxyLoader)
response_data['result'] = user.user_list()
logs.debug(f'[RPC] {method} recieved from {rip}:{rport}')
del user
case 'user.get':
user = RPCUser(ProxyLoader)
uid_or_nickname = params.get('uid_or_nickname', None)
response_data['result'] = user.user_get(uid_or_nickname)
logs.debug(f'[RPC] {method} recieved from {rip}:{rport}')
del user
case 'channel.list':
channel = RPCChannel(ProxyLoader)
response_data['result'] = channel.channel_list()
logs.debug(f'[RPC] {method} recieved from {rip}:{rport}')
del channel
case 'command.list':
command = RPCCommand(ProxyLoader)
response_data['result'] = command.command_list()
logs.debug(f'[RPC] {method} recieved from {rip}:{rport}')
del command
case 'command.get.by.module':
command = RPCCommand(ProxyLoader)
module_name = params.get('name', None)
response_data['result'] = command.command_get_by_module(module_name)
logs.debug(f'[RPC] {method} recieved from {rip}:{rport}')
del command
case 'command.get.by.name':
command = RPCCommand(ProxyLoader)
command_name = params.get('name', None)
response_data['result'] = command.command_get_by_name(command_name)
logs.debug(f'[RPC] {method} recieved from {rip}:{rport}')
del command
case _:
response_data['error'] = create_error_response(JSONRPCErrorCode.METHOD_NOT_FOUND)
logs.debug(f'[RPC ERROR] {method} recieved from {rip}:{rport}')
http_code = 404
self.send_response(http_code)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(response_data).encode('utf-8'))
return None
def do_GET(self):
self.server_version = 'Defender6'
self.sys_version = ProxyLoader.Config.CURRENT_VERSION
content_length = int(self.headers['Content-Length'])
body = self.rfile.read(content_length)
request_data: dict = json.loads(body)
if not self.authenticate(request_data):
return None
response_data = {'jsonrpc': '2.0', 'id': request_data.get('id', 321),
'error': create_error_response(JSONRPCErrorCode.INVALID_REQUEST)}
self.send_response(404)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(response_data).encode('utf-8'))
return None
def authenticate(self, request_data: dict) -> bool:
logs = ProxyLoader.Logs
auth = self.headers.get('Authorization', None)
if auth is None:
self.send_auth_error(request_data)
return False
# Authorization header format: Basic base64(username:password)
auth_type, auth_string = auth.split(' ', 1)
if auth_type.lower() != 'basic':
self.send_auth_error(request_data)
return False
try:
# Decode the base64-encoded username:password
decoded_credentials = base64.b64decode(auth_string).decode('utf-8')
username, password = decoded_credentials.split(":", 1)
# Check the username and password.
for rpcuser in ProxyLoader.Irc.Config.RPC_USERS:
if rpcuser.get('USERNAME', None) == username and rpcuser.get('PASSWORD', None) == password:
return True
self.send_auth_error(request_data)
return False
except Exception as e:
self.send_auth_error(request_data)
logs.error(e)
return False
def send_auth_error(self, request_data: dict) -> None:
response_data = {
'jsonrpc': '2.0',
'id': request_data.get('id', 123),
'error': create_error_response(JSONRPCErrorCode.AUTHENTICATION_ERROR)
}
self.send_response(401)
self.send_header('WWW-Authenticate', 'Basic realm="Authorization Required"')
self.end_headers()
self.wfile.write(json.dumps(response_data).encode('utf-8'))
class JSONRPCServer:
def __init__(self, loader: 'Loader'):
global ProxyLoader
ProxyLoader = loader
self._Loader = loader
self._Base = loader.Base
self._Logs = loader.Logs
self.rpc_server: Optional[HTTPServer] = None
self.connected: bool = False
def start_server(self, server_class=HTTPServer, handler_class=RPCRequestHandler, *, hostname: str = 'localhost', port: int = 5000):
logging.getLogger('http.server').setLevel(logging.CRITICAL)
server_address = (hostname, port)
self.rpc_server = server_class(server_address, handler_class)
self._Logs.debug(f"Server ready on http://{hostname}:{port}...")
self._Base.create_thread(self.thread_start_rpc_server, (), True)
def thread_start_rpc_server(self) -> None:
self._Loader.Irc.Protocol.send_priv_msg(
self._Loader.Config.SERVICE_NICKNAME, "Defender RPC Server has started successfuly!", self._Loader.Config.SERVICE_CHANLOG
)
self.connected = True
self.rpc_server.serve_forever()
ProxyLoader.Logs.debug(f"RPC Server down!")
def stop_server(self):
self._Base.create_thread(self.thread_stop_rpc_server)
def thread_stop_rpc_server(self):
self.rpc_server.shutdown()
ProxyLoader.Logs.debug(f"RPC Server shutdown!")
self.rpc_server.server_close()
ProxyLoader.Logs.debug(f"RPC Server clean-up!")
self._Base.garbage_collector_thread()
self._Loader.Irc.Protocol.send_priv_msg(
self._Loader.Config.SERVICE_NICKNAME, "Defender RPC Server has stopped successfuly!", self._Loader.Config.SERVICE_CHANLOG
)
self.connected = False
class JSONRPCErrorCode(Enum):
PARSE_ERROR = -32700 # Syntax error in the request (malformed JSON)
INVALID_REQUEST = -32600 # Invalid Request (incorrect structure or missing fields)
METHOD_NOT_FOUND = -32601 # Method not found (the requested method does not exist)
INVALID_PARAMS = -32602 # Invalid Params (the parameters provided are incorrect)
INTERNAL_ERROR = -32603 # Internal Error (an internal server error occurred)
# Custom application-specific errors (beyond standard JSON-RPC codes)
CUSTOM_ERROR = 1001 # Custom application-defined error (e.g., user not found)
AUTHENTICATION_ERROR = 1002 # Authentication failure (e.g., invalid credentials)
PERMISSION_ERROR = 1003 # Permission error (e.g., user does not have access to this method)
RESOURCE_NOT_FOUND = 1004 # Resource not found (e.g., the requested resource does not exist)
DUPLICATE_REQUEST = 1005 # Duplicate request (e.g., a similar request has already been processed)
def description(self):
"""Returns a description associated with each error code"""
descriptions = {
JSONRPCErrorCode.PARSE_ERROR: "The JSON request is malformed.",
JSONRPCErrorCode.INVALID_REQUEST: "The request is invalid (missing or incorrect fields).",
JSONRPCErrorCode.METHOD_NOT_FOUND: "The requested method could not be found.",
JSONRPCErrorCode.INVALID_PARAMS: "The parameters provided are invalid.",
JSONRPCErrorCode.INTERNAL_ERROR: "An internal error occurred on the server.",
JSONRPCErrorCode.CUSTOM_ERROR: "A custom error defined by the application.",
JSONRPCErrorCode.AUTHENTICATION_ERROR: "User authentication failed.",
JSONRPCErrorCode.PERMISSION_ERROR: "User does not have permission to access this method.",
JSONRPCErrorCode.RESOURCE_NOT_FOUND: "The requested resource could not be found.",
JSONRPCErrorCode.DUPLICATE_REQUEST: "The request is a duplicate or is already being processed.",
}
return descriptions.get(self, "Unknown error")
def create_error_response(error_code: JSONRPCErrorCode, details: dict = None) -> dict[str, str]:
"""Create a JSON-RPC error!"""
response = {
"code": error_code.value,
"message": error_code.description(),
}
if details:
response["data"] = details
return response

View File

@@ -0,0 +1,12 @@
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from core.loader import Loader
class RPCChannel:
def __init__(self, loader: 'Loader'):
self._Loader = loader
self._Channel = loader.Channel
def channel_list(self) -> list[dict]:
return [chan.to_dict() for chan in self._Channel.UID_CHANNEL_DB]

View File

@@ -0,0 +1,21 @@
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from core.loader import Loader
class RPCCommand:
def __init__(self, loader: 'Loader'):
self._Loader = loader
self._Command = loader.Commands
def command_list(self) -> list[dict]:
return [command.to_dict() for command in self._Command.DB_COMMANDS]
def command_get_by_module(self, module_name: str) -> list[dict]:
return [command.to_dict() for command in self._Command.DB_COMMANDS if command.module_name.lower() == module_name.lower()]
def command_get_by_name(self, command_name: str) -> dict:
for command in self._Command.DB_COMMANDS:
if command.command_name.lower() == command_name.lower():
return command.to_dict()
return {}

View File

@@ -0,0 +1,30 @@
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from core.loader import Loader
from core.definition import MUser
class RPCUser:
def __init__(self, loader: 'Loader'):
self._Loader = loader
self._User = loader.User
def user_list(self) -> list[dict]:
users = self._User.UID_DB.copy()
copy_users: list['MUser'] = []
for user in users:
copy_user = user.copy()
copy_user.connexion_datetime = copy_user.connexion_datetime.strftime('%d-%m-%Y')
copy_users.append(copy_user)
return [user.to_dict() for user in copy_users]
def user_get(self, uidornickname: str) -> Optional[dict]:
user = self._User.get_user(uidornickname)
if user:
user_copy = user.copy()
user_copy.connexion_datetime = user_copy.connexion_datetime.strftime('%d-%m-%Y')
return user_copy.to_dict()
return None

View File

@@ -1,6 +1,6 @@
from datetime import datetime from datetime import datetime
from json import dumps from json import dumps
from dataclasses import dataclass, field, asdict, fields from dataclasses import dataclass, field, asdict, fields, replace
from typing import Literal, Any, Optional from typing import Literal, Any, Optional
from os import sep from os import sep
@@ -15,6 +15,10 @@ class MainModel:
"""Return the object of a dataclass a json str.""" """Return the object of a dataclass a json str."""
return dumps(self.to_dict()) return dumps(self.to_dict())
def copy(self):
"""Return the object of a dataclass a json str."""
return replace(self)
def get_attributes(self) -> list[str]: def get_attributes(self) -> list[str]:
"""Return a list of attributes name""" """Return a list of attributes name"""
return [f.name for f in fields(self)] return [f.name for f in fields(self)]
@@ -205,6 +209,9 @@ class MConfig(MainModel):
PASSWORD: str = "password" PASSWORD: str = "password"
"""The password of the admin of the service""" """The password of the admin of the service"""
RPC_USERS: list[dict] = field(default_factory=list)
"""The Defender rpc users"""
JSONRPC_URL: str = None JSONRPC_URL: str = None
"""The RPC url, if local https://127.0.0.1:PORT/api should be fine""" """The RPC url, if local https://127.0.0.1:PORT/api should be fine"""

View File

@@ -126,6 +126,8 @@ class Irc:
self.build_command(4, 'core', 'rehash', 'Reload the configuration file without restarting') self.build_command(4, 'core', 'rehash', 'Reload the configuration file without restarting')
self.build_command(4, 'core', 'raw', 'Send a raw command directly to the IRC server') self.build_command(4, 'core', 'raw', 'Send a raw command directly to the IRC server')
self.build_command(4, 'core', 'print_vars', 'Print users in a file.') self.build_command(4, 'core', 'print_vars', 'Print users in a file.')
self.build_command(4, 'core', 'start_rpc', 'Start defender jsonrpc server')
self.build_command(4, 'core', 'stop_rpc', 'Stop defender jsonrpc server')
# Define the IrcSocket object # Define the IrcSocket object
self.IrcSocket: Optional[Union[socket.socket, SSLSocket]] = None self.IrcSocket: Optional[Union[socket.socket, SSLSocket]] = None
@@ -1272,5 +1274,11 @@ class Irc:
return None return None
case 'start_rpc':
self.Loader.RpcServer.start_server()
case 'stop_rpc':
self.Loader.RpcServer.stop_server()
case _: case _:
pass pass

View File

@@ -8,6 +8,7 @@ import core.base as base_mod
import core.module as module_mod import core.module as module_mod
import core.classes.modules.commands as commands_mod import core.classes.modules.commands as commands_mod
import core.classes.modules.config as conf_mod import core.classes.modules.config as conf_mod
import core.classes.modules.rpc.rpc as rpc_mod
import core.irc as irc import core.irc as irc
import core.classes.protocols.factory as factory import core.classes.protocols.factory as factory
@@ -26,6 +27,8 @@ class Loader:
self.LoggingModule: logs = logs self.LoggingModule: logs = logs
self.RpcServerModule: rpc_mod = rpc_mod
self.Utils: utils = utils self.Utils: utils = utils
# Load Classes # Load Classes
@@ -69,6 +72,8 @@ class Loader:
self.PFactory: factory.ProtocolFactorty = factory.ProtocolFactorty(self.Irc) self.PFactory: factory.ProtocolFactorty = factory.ProtocolFactorty(self.Irc)
self.RpcServer: rpc_mod.JSONRPCServer = rpc_mod.JSONRPCServer(self)
self.Base.init() self.Base.init()
self.Logs.debug(self.Utils.tr("Loader %s success", __name__)) self.Logs.debug(self.Utils.tr("Loader %s success", __name__))

View File

@@ -177,7 +177,7 @@ def generate_random_string(lenght: int) -> str:
return randomize return randomize
def hash_password(password: str, algorithm: Literal["md5, sha3_512"] = 'md5') -> str: def hash_password(password: str, algorithm: Literal["md5", "sha3_512"] = 'md5') -> str:
"""Return the crypted password following the selected algorithm """Return the crypted password following the selected algorithm
Args: Args:
@@ -190,16 +190,16 @@ def hash_password(password: str, algorithm: Literal["md5, sha3_512"] = 'md5') ->
match algorithm: match algorithm:
case 'md5': case 'md5':
password = md5(password.encode()).hexdigest() hashed_password = md5(password.encode()).hexdigest()
return password return hashed_password
case 'sha3_512': case 'sha3_512':
password = sha3_512(password.encode()).hexdigest() hashed_password = sha3_512(password.encode()).hexdigest()
return password return hashed_password
case _: case _:
password = md5(password.encode()).hexdigest() hashed_password = md5(password.encode()).hexdigest()
return password return hashed_password
def get_all_modules() -> list[str]: def get_all_modules() -> list[str]:
"""Get list of all main modules """Get list of all main modules

View File

@@ -10,7 +10,7 @@ from core import install
############################################# #############################################
try: try:
install.update_packages() # install.update_packages()
from core.loader import Loader from core.loader import Loader
loader = Loader() loader = Loader()
loader.Irc.init_irc() loader.Irc.init_irc()

View File

@@ -152,8 +152,13 @@ def handle_on_nick(uplink: 'Defender', srvmsg: list[str]):
confmodel (ModConfModel): The Module Configuration confmodel (ModConfModel): The Module Configuration
""" """
p = uplink.Protocol p = uplink.Protocol
parser = p.parse_nick(srvmsg) u, new_nickname, timestamp = p.parse_nick(srvmsg)
uid = uplink.Loader.Utils.clean_uid(parser.get('uid', None))
if u is None:
uplink.Logs.error(f"[USER OBJ ERROR {timestamp}] - {srvmsg}")
return None
uid = u.uid
confmodel = uplink.ModConfig confmodel = uplink.ModConfig
get_reputation = uplink.Reputation.get_reputation(uid) get_reputation = uplink.Reputation.get_reputation(uid)
@@ -166,7 +171,7 @@ def handle_on_nick(uplink: 'Defender', srvmsg: list[str]):
# Update the new nickname # Update the new nickname
oldnick = get_reputation.nickname oldnick = get_reputation.nickname
newnickname = parser.get('newnickname', None) newnickname = new_nickname
get_reputation.nickname = newnickname get_reputation.nickname = newnickname
# If ban in all channel is ON then unban old nickname an ban the new nickname # If ban in all channel is ON then unban old nickname an ban the new nickname