Plumb connect_session call into sesman and sesexec
This commit is contained in:
+109
-3
@@ -31,6 +31,7 @@
|
||||
#include "trans.h"
|
||||
#include "os_calls.h"
|
||||
#include "eicp.h"
|
||||
#include "ercp.h"
|
||||
#include "scp.h"
|
||||
|
||||
#include "display_utils.h"
|
||||
@@ -301,9 +302,12 @@ logout_scp_list_item(struct scp_list_item *sli)
|
||||
{
|
||||
if (sli->login_state != E_SLI_LOGIN_NOT_LOGGED_IN)
|
||||
{
|
||||
(void)eicp_send_logout_request(sli->sesexec_trans);
|
||||
trans_delete(sli->sesexec_trans);
|
||||
sli->sesexec_trans = NULL;
|
||||
if (sli->sesexec_trans != NULL)
|
||||
{
|
||||
(void)eicp_send_logout_request(sli->sesexec_trans);
|
||||
trans_delete(sli->sesexec_trans);
|
||||
sli->sesexec_trans = NULL;
|
||||
}
|
||||
sli->uid = (uid_t) -1;
|
||||
g_free(sli->username);
|
||||
sli->username = NULL;
|
||||
@@ -469,6 +473,9 @@ process_create_session_request(struct scp_list_item *sli)
|
||||
if (s_item != NULL)
|
||||
{
|
||||
// Found an existing session
|
||||
LOG(LOG_LEVEL_INFO,
|
||||
"A suitable session on display :%d is already active",
|
||||
s_item->display);
|
||||
display = s_item->display;
|
||||
guid = s_item->guid;
|
||||
}
|
||||
@@ -476,15 +483,22 @@ process_create_session_request(struct scp_list_item *sli)
|
||||
else if (g_cfg->sess.max_sessions > 0 &&
|
||||
session_list_get_count() >= g_cfg->sess.max_sessions)
|
||||
{
|
||||
LOG(LOG_LEVEL_ERROR,
|
||||
"The maximum number of sessions has been reached");
|
||||
status = E_SCP_SCREATE_MAX_REACHED;
|
||||
}
|
||||
else if ((display = get_free_display()) < 0)
|
||||
{
|
||||
LOG(LOG_LEVEL_ERROR,
|
||||
"No free display can be found for a new session");
|
||||
status = E_SCP_SCREATE_NO_DISPLAY;
|
||||
}
|
||||
// Create a socket dir for this user
|
||||
else if (create_xrdp_socket_path(sli->uid) != 0)
|
||||
{
|
||||
LOG(LOG_LEVEL_ERROR,
|
||||
"Can't create a socket directory for UID %d",
|
||||
(int)sli->uid);
|
||||
status = E_SCP_SCREATE_GENERAL_ERROR;
|
||||
}
|
||||
// Create a sesexec process if we don't have one (UDS login)
|
||||
@@ -511,6 +525,8 @@ process_create_session_request(struct scp_list_item *sli)
|
||||
else
|
||||
{
|
||||
// Pass the session create request to sesexec
|
||||
LOG(LOG_LEVEL_INFO,
|
||||
"Passing session creation request to sesexec");
|
||||
int eicp_stat;
|
||||
eicp_stat = eicp_send_create_session_request(
|
||||
sli->sesexec_trans,
|
||||
@@ -554,6 +570,86 @@ process_create_session_request(struct scp_list_item *sli)
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
static int
|
||||
process_connect_session_request(struct scp_list_item *sli)
|
||||
{
|
||||
int rv;
|
||||
/* Client parameters describing new session */
|
||||
struct guid guid;
|
||||
unsigned int flags;
|
||||
enum scp_sconnect_status status = E_SCP_SCONNECT_OK;
|
||||
|
||||
rv = scp_get_connect_session_request(sli->client_trans, &guid, &flags);
|
||||
|
||||
if (rv == 0)
|
||||
{
|
||||
if (sli->login_state == E_SLI_LOGIN_NOT_LOGGED_IN)
|
||||
{
|
||||
status = E_SCP_SCONNECT_NOT_LOGGED_IN;
|
||||
}
|
||||
else
|
||||
{
|
||||
struct session_item *s_item = session_list_get_byguid(&guid);
|
||||
if (s_item == NULL)
|
||||
{
|
||||
LOG(LOG_LEVEL_ERROR,
|
||||
"User %s tried to connect to non-existent session",
|
||||
sli->username);
|
||||
status = E_SCP_SCONNECT_NO_SUCH_GUID;
|
||||
}
|
||||
else if (s_item->uid != sli->uid)
|
||||
{
|
||||
LOG(LOG_LEVEL_ERROR,
|
||||
"User %s (UID %d) denied access to session for UID %d",
|
||||
sli->username, sli->uid, s_item->uid);
|
||||
status = E_SCP_SCONNECT_NO_SUCH_GUID;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Don't log the GUID
|
||||
LOG(LOG_LEVEL_INFO,
|
||||
"Forwarding request from %s to connect to a session",
|
||||
sli->username);
|
||||
// Pass the session create request to sesexec
|
||||
int ercp_stat;
|
||||
ercp_stat = ercp_send_connect_session_request(
|
||||
s_item->sesexec_trans,
|
||||
sli->client_trans->sck,
|
||||
flags);
|
||||
|
||||
if (ercp_stat != 0)
|
||||
{
|
||||
// The sesexec transport is broken. That's dealt with
|
||||
// elsewhere.
|
||||
LOG(LOG_LEVEL_ERROR,
|
||||
"Can't ask sesexec to connect to a session");
|
||||
status = E_SCP_SCONNECT_GENERAL_ERROR;
|
||||
}
|
||||
else
|
||||
{
|
||||
status = E_SCP_SCONNECT_OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send anything other than a successful connection request
|
||||
// back to the client
|
||||
if (status != E_SCP_SCONNECT_OK)
|
||||
{
|
||||
rv = scp_send_connect_session_response(sli->client_trans,
|
||||
status, -1, -1);
|
||||
}
|
||||
}
|
||||
|
||||
// This call is always the last thing on the SCP connection.
|
||||
logout_scp_list_item(sli); // Remove any sesexec process used for auth
|
||||
sli->dispatcher_action = E_SLD_TERMINATE_SCP_CONN;
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
static int
|
||||
process_list_sessions_request(struct scp_list_item *sli)
|
||||
{
|
||||
@@ -639,6 +735,12 @@ process_close_connection_request(struct scp_list_item *sli)
|
||||
LOG(LOG_LEVEL_INFO, "Received request to close connection from %s",
|
||||
sli->peername);
|
||||
|
||||
/* Make sure we're logged out */
|
||||
if (sli->login_state != E_SLI_LOGIN_NOT_LOGGED_IN)
|
||||
{
|
||||
logout_scp_list_item(sli);
|
||||
}
|
||||
|
||||
/* Expecting no more client messages. Close the connection
|
||||
* after returning from this callback */
|
||||
sli->dispatcher_action = E_SLD_TERMINATE_SCP_CONN;
|
||||
@@ -674,6 +776,10 @@ scp_process(struct scp_list_item *sli)
|
||||
rv = process_create_session_request(sli);
|
||||
break;
|
||||
|
||||
case E_SCP_CONNECT_SESSION_REQUEST:
|
||||
rv = process_connect_session_request(sli);
|
||||
break;
|
||||
|
||||
case E_SCP_LIST_SESSIONS_REQUEST:
|
||||
rv = process_list_sessions_request(sli);
|
||||
break;
|
||||
|
||||
@@ -30,7 +30,10 @@
|
||||
|
||||
#include "arch.h"
|
||||
|
||||
#include "login_info.h"
|
||||
#include "scp.h"
|
||||
#include "sesexec.h"
|
||||
#include "os_calls.h"
|
||||
#include "session.h"
|
||||
#include "trans.h"
|
||||
|
||||
@@ -38,11 +41,99 @@
|
||||
#include "ercp_server.h"
|
||||
|
||||
/******************************************************************************/
|
||||
static int
|
||||
handle_session_reconnect_event(struct trans *self)
|
||||
static enum scp_sconnect_status
|
||||
get_session_fds(struct session_data *sd, unsigned int scp_flags,
|
||||
int *display_fd, int *chan_fd)
|
||||
{
|
||||
session_reconnect(g_login_info, g_session_data);
|
||||
return 0;
|
||||
enum scp_sconnect_status result = E_SCP_SCONNECT_OK;
|
||||
|
||||
if ((*display_fd = session_get_display_server_fd(g_login_info, sd)) < 0)
|
||||
{
|
||||
result = E_SCP_SCONNECT_SERVER_FAIL;
|
||||
}
|
||||
else if ((scp_flags & E_SCP_SCONNECT_FLAG_NEED_CHANSRV) == 0)
|
||||
{
|
||||
// Don't need to try to connect to chansrv
|
||||
*chan_fd = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If this fails, it's inconvenient, but not a show-stopper
|
||||
*chan_fd = session_get_chansrv_fd(g_login_info, sd);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
static int
|
||||
handle_connect_session_request(struct trans *self)
|
||||
{
|
||||
int scp_fd = -1;
|
||||
unsigned int scp_flags;
|
||||
|
||||
int rv = ercp_get_connect_session_request(self, &scp_fd, &scp_flags);
|
||||
if (rv == 0)
|
||||
{
|
||||
struct trans *scp_trans;
|
||||
|
||||
if ((scp_trans = scp_init_trans_from_fd(scp_fd,
|
||||
TRANS_TYPE_SERVER,
|
||||
sesexec_is_term)) == NULL)
|
||||
{
|
||||
LOG(LOG_LEVEL_ERROR, "Can't create SCP trans");
|
||||
rv = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
scp_fd = -1; // Don't close this twice!
|
||||
|
||||
// Now we've got a transport we can send data back to
|
||||
// the SCP client
|
||||
enum scp_sconnect_status scp_status;
|
||||
int display_fd = -1;
|
||||
int chan_fd = -1;
|
||||
scp_status = get_session_fds(g_session_data, scp_flags,
|
||||
&display_fd, &chan_fd);
|
||||
rv = scp_send_connect_session_response(scp_trans, scp_status,
|
||||
display_fd, chan_fd);
|
||||
|
||||
if (rv == 0 && scp_status == E_SCP_SCONNECT_OK)
|
||||
{
|
||||
// Don't run the reconnect script on the first connect
|
||||
if (session_increment_connect_count(g_session_data) == 0)
|
||||
{
|
||||
LOG(LOG_LEVEL_INFO, "User %s has connected to a session",
|
||||
g_login_info->username);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LOG_LEVEL_INFO, "User %s has reconnected to a session",
|
||||
g_login_info->username);
|
||||
session_run_reconnect_script(g_login_info, g_session_data);
|
||||
}
|
||||
}
|
||||
|
||||
// Regardless of the result of the send, we must close all
|
||||
// our copies of file descriptors.
|
||||
if (display_fd >= 0)
|
||||
{
|
||||
g_file_close(display_fd);
|
||||
}
|
||||
if (chan_fd >= 0)
|
||||
{
|
||||
g_file_close(chan_fd);
|
||||
}
|
||||
trans_delete(scp_trans);
|
||||
}
|
||||
}
|
||||
|
||||
if (scp_fd >= 0)
|
||||
{
|
||||
g_file_close(scp_fd);
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
@@ -54,8 +145,8 @@ ercp_server(struct trans *self)
|
||||
|
||||
switch ((msgno = ercp_msg_in_get_msgno(self)))
|
||||
{
|
||||
case E_ERCP_SESSION_RECONNECT_EVENT:
|
||||
rv = handle_session_reconnect_event(self);
|
||||
case E_ERCP_CONNECT_SESSION_REQUEST:
|
||||
rv = handle_connect_session_request(self);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
+136
-8
@@ -33,6 +33,7 @@
|
||||
#include "config_ac.h"
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "arch.h"
|
||||
@@ -49,6 +50,7 @@
|
||||
#include "sesexec.h"
|
||||
#include "sessionrecord.h"
|
||||
#include "string_calls.h"
|
||||
#include "trans.h"
|
||||
#include "xauth.h"
|
||||
#include "xwait.h"
|
||||
#include "xrdp_sockets.h"
|
||||
@@ -59,6 +61,7 @@ struct session_data
|
||||
pid_t win_mgr; ///< PID of window manager
|
||||
pid_t chansrv; //< PID of chansrv
|
||||
time_t start_time;
|
||||
unsigned int connect_count;
|
||||
struct session_parameters params;
|
||||
// Flexible array member used to store strings in params and ip_addr;
|
||||
#ifdef __cplusplus
|
||||
@@ -95,6 +98,7 @@ session_data_new(const struct session_parameters *sp)
|
||||
sd->x_server = -1;
|
||||
sd->chansrv = -1;
|
||||
sd->start_time = 0;
|
||||
sd->connect_count = 0;
|
||||
|
||||
/* Copy all the non-string session parameters... */
|
||||
sd->params = *sp;
|
||||
@@ -184,7 +188,7 @@ dumpItemsToString(struct list *self, char *outstr, int len)
|
||||
|
||||
/******************************************************************************/
|
||||
static void
|
||||
start_chansrv(struct login_info *login_info,
|
||||
start_chansrv(const struct login_info *login_info,
|
||||
const struct session_parameters *s)
|
||||
{
|
||||
struct list *chansrv_params = list_create();
|
||||
@@ -222,7 +226,7 @@ start_chansrv(struct login_info *login_info,
|
||||
|
||||
/******************************************************************************/
|
||||
static void
|
||||
start_window_manager(struct login_info *login_info,
|
||||
start_window_manager(const struct login_info *login_info,
|
||||
const struct session_parameters *s)
|
||||
{
|
||||
char text[256];
|
||||
@@ -471,7 +475,7 @@ prepare_xvnc_xserver_params(const struct session_parameters *s,
|
||||
/******************************************************************************/
|
||||
/* Either execs the X server, or returns */
|
||||
static void
|
||||
start_x_server(struct login_info *login_info,
|
||||
start_x_server(const struct login_info *login_info,
|
||||
const struct session_parameters *s)
|
||||
{
|
||||
char authfile[256]; /* The filename for storing xauth information */
|
||||
@@ -575,8 +579,8 @@ start_x_server(struct login_info *login_info,
|
||||
* Simple helper process to fork a child and log errors */
|
||||
static int
|
||||
fork_child(
|
||||
void (*runproc)(struct login_info *, const struct session_parameters *),
|
||||
struct login_info *login_info,
|
||||
void (*runproc)(const struct login_info *, const struct session_parameters *),
|
||||
const struct login_info *login_info,
|
||||
const struct session_parameters *s,
|
||||
pid_t group_pid)
|
||||
{
|
||||
@@ -1065,6 +1069,20 @@ session_get_start_time(const struct session_data *sd)
|
||||
return (sd == NULL) ? 0 : sd->start_time;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
unsigned int
|
||||
session_get_connect_count(const struct session_data *sd)
|
||||
{
|
||||
return (sd == NULL) ? 0 : sd->connect_count;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
unsigned int
|
||||
session_increment_connect_count(struct session_data *sd)
|
||||
{
|
||||
return (sd == NULL) ? 0 : sd->connect_count++;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
const struct session_parameters *
|
||||
session_get_parameters(const struct session_data *sd)
|
||||
@@ -1110,7 +1128,7 @@ session_send_term(struct session_data *sd, int wait_for_all)
|
||||
|
||||
/******************************************************************************/
|
||||
static void
|
||||
start_reconnect_script(struct login_info *login_info,
|
||||
start_reconnect_script(const struct login_info *login_info,
|
||||
const struct session_parameters *s)
|
||||
{
|
||||
env_set_user(login_info->uid, 0, s->display,
|
||||
@@ -1143,8 +1161,8 @@ start_reconnect_script(struct login_info *login_info,
|
||||
|
||||
/******************************************************************************/
|
||||
void
|
||||
session_reconnect(struct login_info *login_info,
|
||||
struct session_data *sd)
|
||||
session_run_reconnect_script(const struct login_info *login_info,
|
||||
const struct session_data *sd)
|
||||
{
|
||||
if (fork_child(start_reconnect_script,
|
||||
login_info, &sd->params, sd->x_server) < 0)
|
||||
@@ -1152,3 +1170,113 @@ session_reconnect(struct login_info *login_info,
|
||||
LOG(LOG_LEVEL_ERROR, "Failed to fork for session reconnection script");
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
int
|
||||
session_get_display_server_fd(const struct login_info *login_info,
|
||||
const struct session_data *sd)
|
||||
{
|
||||
char portname[XRDP_SOCKETS_MAXPATH];
|
||||
const char *localhost = "localhost"; // Ignored for TRANS_MODE_UNIX
|
||||
int socket_mode;
|
||||
|
||||
int rv = -1;
|
||||
|
||||
if (sd->x_server <= 0)
|
||||
{
|
||||
LOG(LOG_LEVEL_ERROR,
|
||||
"Request to connect to display server :%u"
|
||||
" which has exited", sd->params.display);
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (sd->params.type)
|
||||
{
|
||||
case SCP_SESSION_TYPE_XVNC:
|
||||
socket_mode = TRANS_MODE_TCP;
|
||||
snprintf(portname, sizeof(portname), "%u",
|
||||
5900 + sd->params.display);
|
||||
break;
|
||||
|
||||
case SCP_SESSION_TYPE_XVNC_UDS:
|
||||
case SCP_SESSION_TYPE_XORG:
|
||||
socket_mode = TRANS_MODE_UNIX;
|
||||
snprintf(portname, sizeof(portname), XRDP_X11RDP_STR,
|
||||
login_info->uid, (int)sd->params.display);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG(LOG_LEVEL_ERROR, "Unsupported session type %d for connect",
|
||||
sd->params.type);
|
||||
portname[0] = '\0';
|
||||
}
|
||||
|
||||
if (portname[0] != '\0')
|
||||
{
|
||||
// Use the transport library to get the fd
|
||||
struct trans *t = trans_create(socket_mode, 8 * 8192, 8192);
|
||||
if (t == NULL)
|
||||
{
|
||||
LOG(LOG_LEVEL_ERROR, "Out of memory creating transport");
|
||||
}
|
||||
else if (trans_connect(t, localhost, portname, 3000) != 0)
|
||||
{
|
||||
LOG(LOG_LEVEL_ERROR, "Can't connect to display server :%u [%s]",
|
||||
sd->params.display,
|
||||
g_get_strerror());
|
||||
}
|
||||
else
|
||||
{
|
||||
rv = t->sck;
|
||||
t->sck = -1;
|
||||
}
|
||||
trans_delete(t);
|
||||
}
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
int
|
||||
session_get_chansrv_fd(const struct login_info *login_info,
|
||||
const struct session_data *sd)
|
||||
{
|
||||
char portname[XRDP_SOCKETS_MAXPATH];
|
||||
|
||||
int rv = -1;
|
||||
|
||||
if (sd->chansrv <= 0)
|
||||
{
|
||||
LOG(LOG_LEVEL_ERROR,
|
||||
"Request to connect to chansrv :%u"
|
||||
" which has exited", sd->params.display);
|
||||
}
|
||||
else
|
||||
{
|
||||
snprintf(portname, sizeof(portname),
|
||||
XRDP_CHANSRV_STR, login_info->uid, (int)sd->params.display);
|
||||
|
||||
// Use the transport library to get the fd
|
||||
struct trans *t = trans_create(TRANS_MODE_UNIX, 8192, 8192);
|
||||
if (t == NULL)
|
||||
{
|
||||
LOG(LOG_LEVEL_ERROR, "Out of memory creating transport");
|
||||
}
|
||||
else if (trans_connect(t, NULL, portname, 10 * 1000) != 0)
|
||||
{
|
||||
LOG(LOG_LEVEL_ERROR, "Can't connect to chansrv :%u [%s]",
|
||||
sd->params.display,
|
||||
g_get_strerror());
|
||||
}
|
||||
else
|
||||
{
|
||||
rv = t->sck;
|
||||
t->sck = -1;
|
||||
}
|
||||
trans_delete(t);
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
@@ -108,6 +108,22 @@ session_active(const struct session_data *sd);
|
||||
time_t
|
||||
session_get_start_time(const struct session_data *sd);
|
||||
|
||||
/**
|
||||
* Returns the connect count for an active session
|
||||
* @param sd session_data for this session
|
||||
* @return connect count
|
||||
*/
|
||||
unsigned int
|
||||
session_get_connect_count(const struct session_data *sd);
|
||||
|
||||
/**
|
||||
* Increment the connect count for an active session
|
||||
* @param sd session_data for this session
|
||||
* @return Pre-increment value of the connect count
|
||||
*/
|
||||
unsigned int
|
||||
session_increment_connect_count(struct session_data *sd);
|
||||
|
||||
/**
|
||||
* Returns the parameters used to start the session
|
||||
*
|
||||
@@ -145,7 +161,21 @@ session_data_free(struct session_data *session_data);
|
||||
* Runs the reconnect script for the session
|
||||
*/
|
||||
void
|
||||
session_reconnect(struct login_info *login_info,
|
||||
struct session_data *sd);
|
||||
session_run_reconnect_script(const struct login_info *login_info,
|
||||
const struct session_data *sd);
|
||||
|
||||
/**
|
||||
* Connects a file descriptor to the display server
|
||||
*/
|
||||
int
|
||||
session_get_display_server_fd(const struct login_info *login_info,
|
||||
const struct session_data *sd);
|
||||
|
||||
/**
|
||||
* Connects a file descriptor to chansrv
|
||||
*/
|
||||
int
|
||||
session_get_chansrv_fd(const struct login_info *login_info,
|
||||
const struct session_data *sd);
|
||||
|
||||
#endif // SESSION_H
|
||||
|
||||
@@ -361,6 +361,25 @@ session_list_get_byuid(const uid_t *uid, unsigned int *cnt, unsigned int flags)
|
||||
return sess;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
struct session_item *
|
||||
session_list_get_byguid(const struct guid *guid)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0 ; i < g_session_list->count ; ++i)
|
||||
{
|
||||
struct session_item *si;
|
||||
si = (struct session_item *)list_get_item(g_session_list, i);
|
||||
if (SESSION_IN_USE(si) && GUID_ARE_EQUAL(guid, &si->guid))
|
||||
{
|
||||
return si;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
void
|
||||
free_session_info_list(struct scp_session_info *sesslist, unsigned int cnt)
|
||||
|
||||
@@ -153,6 +153,17 @@ session_list_get_bydata(uid_t uid,
|
||||
struct scp_session_info *
|
||||
session_list_get_byuid(const uid_t *uid, unsigned int *cnt, unsigned int flags);
|
||||
|
||||
/**
|
||||
* @brief retrieves a session by GUID
|
||||
* @param guid GUID of session
|
||||
* @return Pointer to session, or NULL.
|
||||
*
|
||||
* The caller is responsible for checking the client has
|
||||
* permissions to access the session.
|
||||
*/
|
||||
struct session_item *
|
||||
session_list_get_byguid(const struct guid *guid);
|
||||
|
||||
/**
|
||||
*
|
||||
* @brief Frees the result of session_get_byuser()
|
||||
|
||||
Reference in New Issue
Block a user