From 46a64c0f68007370f478ccd5d6b93adfb2fc14cd Mon Sep 17 00:00:00 2001 From: Badlop Date: Mon, 6 Jan 2025 19:05:58 +0100 Subject: [PATCH] New ejabberdctl option CTL_OVER_HTTP This uses an HTTP connection to execute the command, which is way faster than starting an erlang node --- ejabberdctl.cfg.example | 11 +++++++++ ejabberdctl.template | 52 +++++++++++++++++++++++++++++++++-------- src/ejabberd_ctl.erl | 49 +++++++++++++++++++++++++++++++++----- 3 files changed, 96 insertions(+), 16 deletions(-) diff --git a/ejabberdctl.cfg.example b/ejabberdctl.cfg.example index 6887fb4cc..88f99cd78 100644 --- a/ejabberdctl.cfg.example +++ b/ejabberdctl.cfg.example @@ -198,6 +198,17 @@ # #CONTRIB_MODULES_CONF_DIR=/etc/ejabberd/modules +#. +#' CTL_OVER_HTTP: Path to ejabberdctl HTTP listener socket +# +# To speedup ejabberdctl execution time for ejabberd commands, +# you can setup an ejabberd_http listener with ejabberd_ctl handling requests, +# listening in a unix domain socket. +# +# Default: disabled +# +#CTL_OVER_HTTP=sockets/ctl_over_http.sock + #. #' # vim: foldmarker=#',#. foldmethod=marker: diff --git a/ejabberdctl.template b/ejabberdctl.template index 37e018b08..bf590acba 100755 --- a/ejabberdctl.template +++ b/ejabberdctl.template @@ -334,6 +334,47 @@ wait_status() [ $timeout -gt 0 ] } +exec_other_command() +{ + if [ -z "$CTL_OVER_HTTP" ] || [ ! -S "$CTL_OVER_HTTP" ] \ + || [ ! -x "$(command -v curl)" ] || [ -z "$1" ] || [ "$1" = "help" ] \ + || [ "$1" = "mnesia_info_ctl" ]|| [ "$1" = "print_sql_schema" ] ; then + exec_erl "$(uid ctl)" -hidden -noinput \ + -eval 'net_kernel:connect_node('"'$ERLANG_NODE'"')' \ + -s ejabberd_ctl \ + -extra "$ERLANG_NODE" $NO_TIMEOUT "$@" + result=$? + case $result in + 3) help;; + *) :;; + esac + exit $result + else + exec_ctl_over_http_socket "$@" + fi +} + +exec_ctl_over_http_socket() +{ + CARGS='{"ctl-command-line": "'${*}'"}' + TEMPHEADERS=temp-headers.log + curl \ + --unix-socket ${CTL_OVER_HTTP} \ + --header "Content-Type: application/json" \ + --header "Accept: application/json" \ + --data "${CARGS}" \ + --dump-header ${TEMPHEADERS} \ + --no-progress-meter \ + "http://localhost/ctl/${1}" + result=$(sed -n 's/.*status-code: \([0-9]*\).*/\1/p' < $TEMPHEADERS) + rm ${TEMPHEADERS} + case $result in + 2|3) exec_other_command help ${1};; + *) :;; + esac + exit $result +} + # ensure we can change current directory to SPOOL_DIR [ -d "$SPOOL_DIR" ] || exec_cmd mkdir -p "$SPOOL_DIR" cd "$SPOOL_DIR" || { @@ -402,15 +443,6 @@ case $1 in ;; *) set_dist_client - exec_erl "$(uid ctl)" -hidden -noinput \ - -eval 'net_kernel:connect_node('"'$ERLANG_NODE'"')' \ - -s ejabberd_ctl \ - -extra "$ERLANG_NODE" $NO_TIMEOUT "$@" - result=$? - case $result in - 2|3) help;; - *) :;; - esac - exit $result + exec_other_command "$@" ;; esac diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index 1504c2609..9533632f8 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -28,7 +28,7 @@ -behaviour(gen_server). -author('alexey@process-one.net'). --export([start/0, start_link/0, process/1, process2/2]). +-export([start/0, start_link/0, process/1, process/2, process2/2]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -37,6 +37,7 @@ -include("ejabberd_ctl.hrl"). -include("ejabberd_commands.hrl"). +-include("ejabberd_http.hrl"). -include("logger.hrl"). -include("ejabberd_stacktrace.hrl"). @@ -115,15 +116,51 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. %%----------------------------- -%% Process +%% Process http +%%----------------------------- + +-spec process_http([binary()], tuple()) -> {non_neg_integer(), [{binary(), binary()}], string()}. + +process_http([Call], #request{data = Data} = Request) when is_binary(Call) and is_record(Request, request) -> + [{<<"ctl-command-line">>, LineBin}] = extract_args(Data), + LineStrings = string:split(binary_to_list(LineBin), " ", all), + process_http2(LineStrings, ?DEFAULT_VERSION). + +process_http2(["--version", Arg | Args], _) -> + Version = + try + list_to_integer(Arg) + catch _:_ -> + throw({invalid_version, Arg}) + end, + process_http2(Args, Version); + +process_http2(Args, Version) -> + {String, Code} = process2(Args, [], Version), + String2 = case String of + [] -> String; + _ -> [String, "\n"] + end, + {200, [{<<"status-code">>, integer_to_binary(Code)}], String2}. + +%% Be tolerant to make API more easily usable from command-line pipe. +extract_args(<<"\n">>) -> []; +extract_args(Data) -> + Maps = misc:json_decode(Data), + maps:to_list(Maps). + +%%----------------------------- +%% Process command line %%----------------------------- -spec process([string()]) -> non_neg_integer(). process(Args) -> process(Args, ?DEFAULT_VERSION). +-spec process([string() | binary()], non_neg_integer() | tuple()) -> non_neg_integer(). --spec process([string()], non_neg_integer()) -> non_neg_integer(). +process([Call], Request) when is_binary(Call) and is_record(Request, request) -> + process_http([Call], Request); %% The commands status, stop and restart are defined here to ensure %% they are usable even if ejabberd is completely stopped. @@ -232,7 +269,7 @@ process2(Args, AccessCommands, Auth, Version) -> io:format(lists:flatten(["\n" | String]++["\n"])), [CommandString | _] = Args, process(["help" | [CommandString]], Version), - {lists:flatten(String), ?STATUS_ERROR}; + {lists:flatten(String), ?STATUS_USAGE}; {String, Code} when is_list(String) and is_integer(Code) -> {lists:flatten(String), Code}; @@ -271,7 +308,7 @@ try_run_ctp(Args, Auth, AccessCommands, Version) -> try_call_command(Args, Auth, AccessCommands, Version); false -> print_usage(Version), - {"", ?STATUS_USAGE}; + {"", ?STATUS_BADRPC}; Status -> {"", Status} catch @@ -288,7 +325,7 @@ try_run_ctp(Args, Auth, AccessCommands, Version) -> try_call_command(Args, Auth, AccessCommands, Version) -> try call_command(Args, Auth, AccessCommands, Version) of {Reason, wrong_command_arguments} -> - {Reason, ?STATUS_ERROR}; + {Reason, ?STATUS_USAGE}; Res -> Res catch