mirror of
https://github.com/iio612/DEFENDER.git
synced 2026-02-13 11:14:23 +00:00
Creating JSONRPC Server
This commit is contained in:
@@ -14,6 +14,7 @@ REHASH_MODULES = [
|
||||
'core.classes.modules.config',
|
||||
'core.base',
|
||||
'core.classes.modules.commands',
|
||||
'core.classes.modules.rpc',
|
||||
'core.classes.interfaces.iprotocol',
|
||||
'core.classes.interfaces.imodule',
|
||||
'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:
|
||||
need_a_restart = ["SERVEUR_ID"]
|
||||
uplink.Settings.set_cache('db_commands', uplink.Commands.DB_COMMANDS)
|
||||
uplink.Loader.RpcServer.stop_server()
|
||||
|
||||
restart_flag = False
|
||||
config_model_bakcup = uplink.Config
|
||||
mods = REHASH_MODULES
|
||||
@@ -80,7 +83,7 @@ def rehash_service(uplink: 'Irc', nickname: str) -> None:
|
||||
channel=uplink.Config.SERVICE_CHANLOG
|
||||
)
|
||||
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.DEFENDER_INIT = config_model_bakcup.DEFENDER_INIT
|
||||
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
|
||||
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.Loader.Base = uplink.Loader.BaseModule.Base(uplink.Loader)
|
||||
|
||||
240
core/classes/modules/rpc/rpc.py
Normal file
240
core/classes/modules/rpc/rpc.py
Normal 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
|
||||
12
core/classes/modules/rpc/rpc_channel.py
Normal file
12
core/classes/modules/rpc/rpc_channel.py
Normal 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]
|
||||
21
core/classes/modules/rpc/rpc_command.py
Normal file
21
core/classes/modules/rpc/rpc_command.py
Normal 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 {}
|
||||
30
core/classes/modules/rpc/rpc_user.py
Normal file
30
core/classes/modules/rpc/rpc_user.py
Normal 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
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
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 os import sep
|
||||
|
||||
@@ -15,6 +15,10 @@ class MainModel:
|
||||
"""Return the object of a dataclass a json str."""
|
||||
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]:
|
||||
"""Return a list of attributes name"""
|
||||
return [f.name for f in fields(self)]
|
||||
@@ -205,6 +209,9 @@ class MConfig(MainModel):
|
||||
PASSWORD: str = "password"
|
||||
"""The password of the admin of the service"""
|
||||
|
||||
RPC_USERS: list[dict] = field(default_factory=list)
|
||||
"""The Defender rpc users"""
|
||||
|
||||
JSONRPC_URL: str = None
|
||||
"""The RPC url, if local https://127.0.0.1:PORT/api should be fine"""
|
||||
|
||||
|
||||
10
core/irc.py
10
core/irc.py
@@ -126,6 +126,8 @@ class Irc:
|
||||
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', '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
|
||||
self.IrcSocket: Optional[Union[socket.socket, SSLSocket]] = None
|
||||
@@ -372,7 +374,7 @@ class Irc:
|
||||
|
||||
return uptime
|
||||
|
||||
def heartbeat(self, beat:float) -> None:
|
||||
def heartbeat(self, beat: float) -> None:
|
||||
"""Execute certaines commandes de nettoyage toutes les x secondes
|
||||
x étant définit a l'initialisation de cette class (self.beat)
|
||||
|
||||
@@ -1272,5 +1274,11 @@ class Irc:
|
||||
|
||||
return None
|
||||
|
||||
case 'start_rpc':
|
||||
self.Loader.RpcServer.start_server()
|
||||
|
||||
case 'stop_rpc':
|
||||
self.Loader.RpcServer.stop_server()
|
||||
|
||||
case _:
|
||||
pass
|
||||
|
||||
@@ -8,6 +8,7 @@ import core.base as base_mod
|
||||
import core.module as module_mod
|
||||
import core.classes.modules.commands as commands_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.classes.protocols.factory as factory
|
||||
|
||||
@@ -26,6 +27,8 @@ class Loader:
|
||||
|
||||
self.LoggingModule: logs = logs
|
||||
|
||||
self.RpcServerModule: rpc_mod = rpc_mod
|
||||
|
||||
self.Utils: utils = utils
|
||||
|
||||
# Load Classes
|
||||
@@ -69,6 +72,8 @@ class Loader:
|
||||
|
||||
self.PFactory: factory.ProtocolFactorty = factory.ProtocolFactorty(self.Irc)
|
||||
|
||||
self.RpcServer: rpc_mod.JSONRPCServer = rpc_mod.JSONRPCServer(self)
|
||||
|
||||
self.Base.init()
|
||||
|
||||
self.Logs.debug(self.Utils.tr("Loader %s success", __name__))
|
||||
|
||||
@@ -177,7 +177,7 @@ def generate_random_string(lenght: int) -> str:
|
||||
|
||||
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
|
||||
|
||||
Args:
|
||||
@@ -190,16 +190,16 @@ def hash_password(password: str, algorithm: Literal["md5, sha3_512"] = 'md5') ->
|
||||
|
||||
match algorithm:
|
||||
case 'md5':
|
||||
password = md5(password.encode()).hexdigest()
|
||||
return password
|
||||
hashed_password = md5(password.encode()).hexdigest()
|
||||
return hashed_password
|
||||
|
||||
case 'sha3_512':
|
||||
password = sha3_512(password.encode()).hexdigest()
|
||||
return password
|
||||
hashed_password = sha3_512(password.encode()).hexdigest()
|
||||
return hashed_password
|
||||
|
||||
case _:
|
||||
password = md5(password.encode()).hexdigest()
|
||||
return password
|
||||
hashed_password = md5(password.encode()).hexdigest()
|
||||
return hashed_password
|
||||
|
||||
def get_all_modules() -> list[str]:
|
||||
"""Get list of all main modules
|
||||
|
||||
@@ -10,7 +10,7 @@ from core import install
|
||||
#############################################
|
||||
|
||||
try:
|
||||
install.update_packages()
|
||||
# install.update_packages()
|
||||
from core.loader import Loader
|
||||
loader = Loader()
|
||||
loader.Irc.init_irc()
|
||||
|
||||
@@ -152,8 +152,13 @@ def handle_on_nick(uplink: 'Defender', srvmsg: list[str]):
|
||||
confmodel (ModConfModel): The Module Configuration
|
||||
"""
|
||||
p = uplink.Protocol
|
||||
parser = p.parse_nick(srvmsg)
|
||||
uid = uplink.Loader.Utils.clean_uid(parser.get('uid', None))
|
||||
u, new_nickname, timestamp = p.parse_nick(srvmsg)
|
||||
|
||||
if u is None:
|
||||
uplink.Logs.error(f"[USER OBJ ERROR {timestamp}] - {srvmsg}")
|
||||
return None
|
||||
|
||||
uid = u.uid
|
||||
confmodel = uplink.ModConfig
|
||||
|
||||
get_reputation = uplink.Reputation.get_reputation(uid)
|
||||
@@ -166,7 +171,7 @@ def handle_on_nick(uplink: 'Defender', srvmsg: list[str]):
|
||||
|
||||
# Update the new nickname
|
||||
oldnick = get_reputation.nickname
|
||||
newnickname = parser.get('newnickname', None)
|
||||
newnickname = new_nickname
|
||||
get_reputation.nickname = newnickname
|
||||
|
||||
# If ban in all channel is ON then unban old nickname an ban the new nickname
|
||||
|
||||
Reference in New Issue
Block a user