GUACAMOLE-221: Add support for sending multiple params in required.
This commit is contained in:
parent
21a5d9ee62
commit
7369bed22c
@ -803,12 +803,12 @@ int guac_protocol_send_rect(guac_socket* socket, const guac_layer* layer,
|
|||||||
* The guac_socket connection to use.
|
* The guac_socket connection to use.
|
||||||
*
|
*
|
||||||
* @param required
|
* @param required
|
||||||
* The name of the parameter that is required.
|
* A NULL-terminated array of required parameters.
|
||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
* Zero on success, non-zero on error.
|
* Zero on success, non-zero on error.
|
||||||
*/
|
*/
|
||||||
int guac_protocol_send_required(guac_socket* socket, const char* required);
|
int guac_protocol_send_required(guac_socket* socket, const char** required);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a reset instruction over the given guac_socket connection.
|
* Sends a reset instruction over the given guac_socket connection.
|
||||||
|
@ -961,17 +961,33 @@ int guac_protocol_send_rect(guac_socket* socket,
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int guac_protocol_send_required(guac_socket* socket, const char* required) {
|
static int __guac_protocol_send_required(guac_socket* socket,
|
||||||
|
const char** required) {
|
||||||
|
|
||||||
|
if (guac_socket_write_string(socket, "8.required")) return -1;
|
||||||
|
|
||||||
|
for (int i=0; required[i] != NULL; i++) {
|
||||||
|
|
||||||
|
if (guac_socket_write_string(socket, ","))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (__guac_socket_write_length_string(socket, required[i]))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return guac_socket_write_string(socket, ";");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
int guac_protocol_send_required(guac_socket* socket, const char** required) {
|
||||||
|
|
||||||
int ret_val;
|
int ret_val;
|
||||||
|
|
||||||
guac_socket_instruction_begin(socket);
|
guac_socket_instruction_begin(socket);
|
||||||
ret_val =
|
ret_val = __guac_protocol_send_required(socket, required);
|
||||||
guac_socket_write_string(socket, "8.required,")
|
|
||||||
|| __guac_socket_write_length_string(socket, required)
|
|
||||||
|| guac_socket_write_string(socket, ";");
|
|
||||||
|
|
||||||
guac_socket_instruction_end(socket);
|
guac_socket_instruction_end(socket);
|
||||||
|
|
||||||
return ret_val;
|
return ret_val;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ typedef enum guac_rdp_argv_setting {
|
|||||||
/**
|
/**
|
||||||
* The domain to use for connection authentication.
|
* The domain to use for connection authentication.
|
||||||
*/
|
*/
|
||||||
GUAC_RDP_ARGV_SETTING_DOMAIN
|
GUAC_RDP_ARGV_SETTING_DOMAIN,
|
||||||
|
|
||||||
} guac_rdp_argv_setting;
|
} guac_rdp_argv_setting;
|
||||||
|
|
||||||
@ -125,25 +125,28 @@ static int guac_rdp_argv_end_handler(guac_user* user,
|
|||||||
free(settings->username);
|
free(settings->username);
|
||||||
settings->username = malloc(strlen(argv->buffer) * sizeof(char));
|
settings->username = malloc(strlen(argv->buffer) * sizeof(char));
|
||||||
strcpy(settings->username, argv->buffer);
|
strcpy(settings->username, argv->buffer);
|
||||||
pthread_cond_broadcast(&(rdp_client->rdp_cond));
|
rdp_client->rdp_cond_flags ^= GUAC_RDP_COND_FLAG_USERNAME;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GUAC_RDP_ARGV_SETTING_PASSWORD:
|
case GUAC_RDP_ARGV_SETTING_PASSWORD:
|
||||||
free(settings->password);
|
free(settings->password);
|
||||||
settings->password = malloc(strlen(argv->buffer) * sizeof(char));
|
settings->password = malloc(strlen(argv->buffer) * sizeof(char));
|
||||||
strcpy(settings->password, argv->buffer);
|
strcpy(settings->password, argv->buffer);
|
||||||
pthread_cond_broadcast(&(rdp_client->rdp_cond));
|
rdp_client->rdp_cond_flags ^= GUAC_RDP_COND_FLAG_PASSWORD;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GUAC_RDP_ARGV_SETTING_DOMAIN:
|
case GUAC_RDP_ARGV_SETTING_DOMAIN:
|
||||||
free(settings->domain);
|
free(settings->domain);
|
||||||
settings->domain = malloc(strlen(argv->buffer) * sizeof(char));
|
settings->domain = malloc(strlen(argv->buffer) * sizeof(char));
|
||||||
strcpy(settings->domain, argv->buffer);
|
strcpy(settings->domain, argv->buffer);
|
||||||
pthread_cond_broadcast(&(rdp_client->rdp_cond));
|
rdp_client->rdp_cond_flags ^= GUAC_RDP_COND_FLAG_DOMAIN;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!rdp_client->rdp_cond_flags)
|
||||||
|
pthread_cond_signal(&(rdp_client->rdp_cond));
|
||||||
|
|
||||||
free(argv);
|
free(argv);
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
@ -161,6 +161,7 @@ int guac_client_init(guac_client* client, int argc, char** argv) {
|
|||||||
/* Init RDP credential lock and condition */
|
/* Init RDP credential lock and condition */
|
||||||
pthread_mutex_init(&(rdp_client->rdp_credential_lock), &(rdp_client->attributes));
|
pthread_mutex_init(&(rdp_client->rdp_credential_lock), &(rdp_client->attributes));
|
||||||
pthread_cond_init(&(rdp_client->rdp_credential_cond), NULL);;
|
pthread_cond_init(&(rdp_client->rdp_credential_cond), NULL);;
|
||||||
|
rdp_client->rdp_credential_flags = 0;
|
||||||
|
|
||||||
/* Set handlers */
|
/* Set handlers */
|
||||||
client->join_handler = guac_rdp_user_join_handler;
|
client->join_handler = guac_rdp_user_join_handler;
|
||||||
|
@ -230,32 +230,51 @@ static BOOL rdp_freerdp_authenticate(freerdp* instance, char** username,
|
|||||||
guac_client* client = ((rdp_freerdp_context*) context)->client;
|
guac_client* client = ((rdp_freerdp_context*) context)->client;
|
||||||
guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
|
guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
|
||||||
guac_rdp_settings* settings = rdp_client->settings;
|
guac_rdp_settings* settings = rdp_client->settings;
|
||||||
|
char* params[4] = {};
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
if (settings->username == NULL || strcmp(settings->username, "") == 0) {
|
||||||
|
params[i] = "username";
|
||||||
|
rdp_client->rdp_cond_flags |= GUAC_RDP_COND_FLAG_USERNAME;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings->password == NULL || strcmp(settings->password, "") == 0) {
|
||||||
|
params[i] = "password";
|
||||||
|
rdp_client->rdp_cond_flags |= GUAC_RDP_COND_FLAG_PASSWORD;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings->domain == NULL || strcmp(settings->domain, "") == 0) {
|
||||||
|
params[i] = "domain";
|
||||||
|
rdp_client->rdp_cond_flags |= GUAC_RDP_COND_FLAG_DOMAIN;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NULL-terminate the array. */
|
||||||
|
params[i] = NULL;
|
||||||
|
|
||||||
|
if (i > 0) {
|
||||||
|
/* Lock the client thread. */
|
||||||
pthread_mutex_lock(&(rdp_client->rdp_lock));
|
pthread_mutex_lock(&(rdp_client->rdp_lock));
|
||||||
|
|
||||||
while (settings->username == NULL || strcmp(settings->username, "") == 0) {
|
/* Send require params and flush socket. */
|
||||||
guac_protocol_send_required(client->socket, "username");
|
guac_protocol_send_required(client->socket, (const char**) params);
|
||||||
guac_socket_flush(client->socket);
|
guac_socket_flush(client->socket);
|
||||||
|
|
||||||
|
/* Wait for condition. */
|
||||||
pthread_cond_wait(&(rdp_client->rdp_cond), &(rdp_client->rdp_lock));
|
pthread_cond_wait(&(rdp_client->rdp_cond), &(rdp_client->rdp_lock));
|
||||||
|
|
||||||
|
/* Get new values from settings. */
|
||||||
*username = settings->username;
|
*username = settings->username;
|
||||||
}
|
|
||||||
|
|
||||||
while (settings->password == NULL || strcmp(settings->password, "") == 0) {
|
|
||||||
guac_protocol_send_required(client->socket, "password");
|
|
||||||
guac_socket_flush(client->socket);
|
|
||||||
pthread_cond_wait(&(rdp_client->rdp_cond), &(rdp_client->rdp_lock));
|
|
||||||
*password = settings->password;
|
*password = settings->password;
|
||||||
}
|
|
||||||
|
|
||||||
while (settings->domain == NULL || strcmp(settings->domain, "") == 0) {
|
|
||||||
guac_protocol_send_required(client->socket, "domain");
|
|
||||||
guac_socket_flush(client->socket);
|
|
||||||
pthread_cond_wait(&(rdp_client->rdp_cond), &(rdp_client->rdp_lock));
|
|
||||||
*domain = settings->domain;
|
*domain = settings->domain;
|
||||||
|
|
||||||
|
/* Unlock the thread. */
|
||||||
|
pthread_mutex_unlock(&(rdp_client->rdp_lock));
|
||||||
}
|
}
|
||||||
|
|
||||||
pthread_mutex_unlock(&(rdp_client->rdp_lock));
|
/* Always return TRUE allowing connection to retry. */
|
||||||
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,21 @@
|
|||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A flag for tracking if we are waiting conditionally on a username.
|
||||||
|
*/
|
||||||
|
#define GUAC_RDP_COND_FLAG_USERNAME 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A flag for tracking if we are waiting conditionally on a password.
|
||||||
|
*/
|
||||||
|
#define GUAC_RDP_COND_FLAG_PASSWORD 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A flag for tracking if we are waiting conditionally on a domain.
|
||||||
|
*/
|
||||||
|
#define GUAC_RDP_COND_FLAG_DOMAIN 3
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RDP-specific client data.
|
* RDP-specific client data.
|
||||||
*/
|
*/
|
||||||
@ -165,6 +180,12 @@ typedef struct guac_rdp_client {
|
|||||||
*/
|
*/
|
||||||
pthread_cond_t rdp_credential_cond;
|
pthread_cond_t rdp_credential_cond;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flags for tracking events related to the rdp_credential_cond
|
||||||
|
* pthread condition.
|
||||||
|
*/
|
||||||
|
unsigned rdp_credential_flags;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common attributes for locks.
|
* Common attributes for locks.
|
||||||
*/
|
*/
|
||||||
|
@ -75,7 +75,7 @@ static void guac_ssh_get_credential(guac_client *client, char* cred_name) {
|
|||||||
|
|
||||||
pthread_mutex_lock(&(ssh_client->term_channel_lock));
|
pthread_mutex_lock(&(ssh_client->term_channel_lock));
|
||||||
|
|
||||||
guac_protocol_send_required(client->socket, cred_name);
|
guac_protocol_send_required(client->socket, (const char* []) {cred_name, NULL});
|
||||||
guac_socket_flush(client->socket);
|
guac_socket_flush(client->socket);
|
||||||
|
|
||||||
pthread_cond_wait(&(ssh_client->ssh_cond), &(ssh_client->term_channel_lock));
|
pthread_cond_wait(&(ssh_client->ssh_cond), &(ssh_client->term_channel_lock));
|
||||||
|
@ -35,6 +35,11 @@
|
|||||||
*/
|
*/
|
||||||
typedef enum guac_vnc_argv_setting {
|
typedef enum guac_vnc_argv_setting {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The username for the connection.
|
||||||
|
*/
|
||||||
|
GUAC_VNC_ARGV_SETTING_USERNAME,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The password for the connection.
|
* The password for the connection.
|
||||||
*/
|
*/
|
||||||
@ -109,6 +114,19 @@ static int guac_vnc_argv_end_handler(guac_user* user, guac_stream* stream) {
|
|||||||
/* Apply changes to chosen setting */
|
/* Apply changes to chosen setting */
|
||||||
switch (argv->setting) {
|
switch (argv->setting) {
|
||||||
|
|
||||||
|
/* Update username */
|
||||||
|
case GUAC_VNC_ARGV_SETTING_USERNAME:
|
||||||
|
|
||||||
|
/* Update username in settings. */
|
||||||
|
if (settings->username != NULL)
|
||||||
|
free(settings->username);
|
||||||
|
settings->username = malloc(strlen(argv->buffer) * sizeof(char));
|
||||||
|
strcpy(settings->username, argv->buffer);
|
||||||
|
|
||||||
|
/* Remove the username conditional flag. */
|
||||||
|
vnc_client->argv_cond_flags ^= GUAC_VNC_COND_FLAG_USERNAME;
|
||||||
|
break;
|
||||||
|
|
||||||
/* Update password */
|
/* Update password */
|
||||||
case GUAC_VNC_ARGV_SETTING_PASSWORD:
|
case GUAC_VNC_ARGV_SETTING_PASSWORD:
|
||||||
|
|
||||||
@ -118,11 +136,16 @@ static int guac_vnc_argv_end_handler(guac_user* user, guac_stream* stream) {
|
|||||||
settings->password = malloc(strlen(argv->buffer) * sizeof(char));
|
settings->password = malloc(strlen(argv->buffer) * sizeof(char));
|
||||||
strcpy(settings->password, argv->buffer);
|
strcpy(settings->password, argv->buffer);
|
||||||
|
|
||||||
pthread_cond_broadcast(&(vnc_client->argv_cond));
|
/* Remove the password conditional flag. */
|
||||||
|
vnc_client->argv_cond_flags ^= GUAC_VNC_COND_FLAG_PASSWORD;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* If no flags are set, signal the conditional. */
|
||||||
|
if (!vnc_client->argv_cond_flags)
|
||||||
|
pthread_cond_broadcast(&(vnc_client->argv_cond));
|
||||||
|
|
||||||
free(argv);
|
free(argv);
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
@ -134,7 +157,9 @@ int guac_vnc_argv_handler(guac_user* user, guac_stream* stream, char* mimetype,
|
|||||||
guac_vnc_argv_setting setting;
|
guac_vnc_argv_setting setting;
|
||||||
|
|
||||||
/* Allow users to update authentication information */
|
/* Allow users to update authentication information */
|
||||||
if (strcmp(name, "password") == 0)
|
if (strcmp(name, "username") == 0)
|
||||||
|
setting = GUAC_VNC_ARGV_SETTING_USERNAME;
|
||||||
|
else if (strcmp(name, "password") == 0)
|
||||||
setting = GUAC_VNC_ARGV_SETTING_PASSWORD;
|
setting = GUAC_VNC_ARGV_SETTING_PASSWORD;
|
||||||
|
|
||||||
/* No other connection parameters may be updated */
|
/* No other connection parameters may be updated */
|
||||||
|
@ -36,11 +36,23 @@ char* guac_vnc_get_password(rfbClient* client) {
|
|||||||
guac_vnc_client* vnc_client = ((guac_vnc_client*) gc->data);
|
guac_vnc_client* vnc_client = ((guac_vnc_client*) gc->data);
|
||||||
guac_vnc_settings* settings = vnc_client->settings;
|
guac_vnc_settings* settings = vnc_client->settings;
|
||||||
|
|
||||||
|
/* If password isn't around, prompt for it. */
|
||||||
if (settings->password == NULL || strcmp(settings->password, "") == 0) {
|
if (settings->password == NULL || strcmp(settings->password, "") == 0) {
|
||||||
|
/* Lock the thread. */
|
||||||
pthread_mutex_lock(&(vnc_client->argv_lock));
|
pthread_mutex_lock(&(vnc_client->argv_lock));
|
||||||
guac_protocol_send_required(gc->socket, "password");
|
|
||||||
|
/* Send the request for password and flush the socket. */
|
||||||
|
guac_protocol_send_required(gc->socket,
|
||||||
|
(const char* []) {"password", NULL});
|
||||||
guac_socket_flush(gc->socket);
|
guac_socket_flush(gc->socket);
|
||||||
|
|
||||||
|
/* Set the conditional flag. */
|
||||||
|
vnc_client->argv_cond_flags |= GUAC_VNC_COND_FLAG_PASSWORD;
|
||||||
|
|
||||||
|
/* Wait for the condition. */
|
||||||
pthread_cond_wait(&(vnc_client->argv_cond), &(vnc_client->argv_lock));
|
pthread_cond_wait(&(vnc_client->argv_cond), &(vnc_client->argv_lock));
|
||||||
|
|
||||||
|
/* Unlock the thread. */
|
||||||
pthread_mutex_unlock(&(vnc_client->argv_lock));
|
pthread_mutex_unlock(&(vnc_client->argv_lock));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,13 +62,52 @@ char* guac_vnc_get_password(rfbClient* client) {
|
|||||||
|
|
||||||
rfbCredential* guac_vnc_get_credentials(rfbClient* client, int credentialType) {
|
rfbCredential* guac_vnc_get_credentials(rfbClient* client, int credentialType) {
|
||||||
guac_client* gc = rfbClientGetClientData(client, GUAC_VNC_CLIENT_KEY);
|
guac_client* gc = rfbClientGetClientData(client, GUAC_VNC_CLIENT_KEY);
|
||||||
guac_vnc_settings* settings = ((guac_vnc_client*) gc->data)->settings;
|
guac_vnc_client* vnc_client = ((guac_vnc_client*) gc->data);
|
||||||
|
guac_vnc_settings* settings = vnc_client->settings;
|
||||||
|
|
||||||
|
/* Handle request for Username/Password credentials */
|
||||||
if (credentialType == rfbCredentialTypeUser) {
|
if (credentialType == rfbCredentialTypeUser) {
|
||||||
rfbCredential *creds = malloc(sizeof(rfbCredential));
|
rfbCredential *creds = malloc(sizeof(rfbCredential));
|
||||||
|
char* params[2] = {NULL};
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
/* Check if username is null or empty. */
|
||||||
|
if (settings->username == NULL || strcmp(settings->username, "") == 0) {
|
||||||
|
params[i] = "username";
|
||||||
|
i++;
|
||||||
|
vnc_client->argv_cond_flags |= GUAC_VNC_COND_FLAG_USERNAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check if password is null or empty. */
|
||||||
|
if (settings->password == NULL || strcmp(settings->password, "") == 0) {
|
||||||
|
params[i] = "password";
|
||||||
|
i++;
|
||||||
|
vnc_client->argv_cond_flags |= GUAC_VNC_COND_FLAG_PASSWORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If we have empty parameters, request them. */
|
||||||
|
if (i > 0) {
|
||||||
|
/* Lock the thread. */
|
||||||
|
pthread_mutex_lock(&(vnc_client->argv_lock));
|
||||||
|
|
||||||
|
/* Send required parameters to client and flush the socket. */
|
||||||
|
guac_protocol_send_required(gc->socket, (const char**) params);
|
||||||
|
guac_socket_flush(gc->socket);
|
||||||
|
|
||||||
|
/* Wait for the parameters to be returned. */
|
||||||
|
pthread_cond_wait(&(vnc_client->argv_cond), &(vnc_client->argv_lock));
|
||||||
|
|
||||||
|
/* Pull the credentials from updated settings. */
|
||||||
creds->userCredential.username = settings->username;
|
creds->userCredential.username = settings->username;
|
||||||
creds->userCredential.password = settings->password;
|
creds->userCredential.password = settings->password;
|
||||||
|
|
||||||
|
/* Unlock the thread. */
|
||||||
|
pthread_mutex_unlock(&(vnc_client->argv_lock));
|
||||||
|
|
||||||
return creds;
|
return creds;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guac_client_abort(gc, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
|
guac_client_abort(gc, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
|
||||||
|
@ -57,6 +57,7 @@ int guac_client_init(guac_client* client) {
|
|||||||
/* Initialize argv lock and condition */
|
/* Initialize argv lock and condition */
|
||||||
pthread_mutex_init(&(vnc_client->argv_lock), NULL);
|
pthread_mutex_init(&(vnc_client->argv_lock), NULL);
|
||||||
pthread_cond_init(&(vnc_client->argv_cond), NULL);
|
pthread_cond_init(&(vnc_client->argv_cond), NULL);
|
||||||
|
vnc_client->argv_cond_flags = 0;
|
||||||
|
|
||||||
/* Init clipboard */
|
/* Init clipboard */
|
||||||
vnc_client->clipboard = guac_common_clipboard_alloc(GUAC_VNC_CLIPBOARD_MAX_LENGTH);
|
vnc_client->clipboard = guac_common_clipboard_alloc(GUAC_VNC_CLIPBOARD_MAX_LENGTH);
|
||||||
|
@ -45,6 +45,16 @@
|
|||||||
|
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A flag for tracking status of requesting username from client.
|
||||||
|
*/
|
||||||
|
#define GUAC_VNC_COND_FLAG_USERNAME 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A flag for tracking status of requesting password from client.
|
||||||
|
*/
|
||||||
|
#define GUAC_VNC_COND_FLAG_PASSWORD 2
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VNC-specific client data.
|
* VNC-specific client data.
|
||||||
*/
|
*/
|
||||||
@ -72,6 +82,11 @@ typedef struct guac_vnc_client {
|
|||||||
*/
|
*/
|
||||||
pthread_cond_t argv_cond;
|
pthread_cond_t argv_cond;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flags for conditional signaling for argv updates;
|
||||||
|
*/
|
||||||
|
unsigned argv_cond_flags;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The underlying VNC client.
|
* The underlying VNC client.
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user