security and optimizations
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
var classPath = path.resolve(__dirname + path.sep + "includes" + path.sep + "classes" + path.sep) + path.sep;
|
const classPath = path.resolve(__dirname + path.sep + "includes" + path.sep + "classes" + path.sep) + path.sep;
|
||||||
require(classPath + "Prototypes.js");
|
require(classPath + "Prototypes.js");
|
||||||
const { WTVShared, clientShowAlert } = require(classPath + "WTVShared.js");
|
const { WTVShared, clientShowAlert } = require(classPath + "WTVShared.js");
|
||||||
const wtvshared = new WTVShared(); // creates minisrv_config
|
const wtvshared = new WTVShared(); // creates minisrv_config
|
||||||
@@ -60,8 +60,8 @@ function getServiceEnabled(service) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getServiceByPort(port) {
|
function getServiceByPort(port) {
|
||||||
var service_name = null;
|
let service_name;
|
||||||
Object.keys(minisrv_config.services).forEach(function (k) {
|
Object.keys(minisrv_config.services).forEach((k) => {
|
||||||
if (service_name) return;
|
if (service_name) return;
|
||||||
if (minisrv_config.services[k].port) {
|
if (minisrv_config.services[k].port) {
|
||||||
if (port == parseInt(minisrv_config.services[k].port) && getServiceEnabled(k))
|
if (port == parseInt(minisrv_config.services[k].port) && getServiceEnabled(k))
|
||||||
@@ -72,8 +72,8 @@ function getServiceByPort(port) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getServiceByVHost(vhost) {
|
function getServiceByVHost(vhost) {
|
||||||
var service_name = null;
|
let service_name;
|
||||||
Object.keys(minisrv_config.services).forEach(function (k) {
|
Object.keys(minisrv_config.services).forEach((k) => {
|
||||||
if (service_name) return;
|
if (service_name) return;
|
||||||
if (minisrv_config.services[k].vhost) {
|
if (minisrv_config.services[k].vhost) {
|
||||||
if (vhost.toLowerCase() == minisrv_config.services[k].vhost.toLowerCase())
|
if (vhost.toLowerCase() == minisrv_config.services[k].vhost.toLowerCase())
|
||||||
@@ -89,7 +89,7 @@ function getPortByService(service) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSocketServer(socket) {
|
function getSocketServer(socket) {
|
||||||
var server = null;
|
let server;
|
||||||
|
|
||||||
if (socket._server) {
|
if (socket._server) {
|
||||||
if (socket._server._connectionKey) server = socket._server;
|
if (socket._server._connectionKey) server = socket._server;
|
||||||
@@ -150,7 +150,7 @@ function configureService(service_name, service_obj, initial = false) {
|
|||||||
|
|
||||||
service_obj.service_name = service_name;
|
service_obj.service_name = service_name;
|
||||||
if (!service_obj.host) {
|
if (!service_obj.host) {
|
||||||
service_obj.host = service_ip;
|
service_obj.host = minisrv_config.config.service_ip;
|
||||||
}
|
}
|
||||||
if (service_obj.port && !service_obj.nobind && initial) {
|
if (service_obj.port && !service_obj.nobind && initial) {
|
||||||
if (service_obj.pc_services) pc_ports.push(service_obj.port);
|
if (service_obj.pc_services) pc_ports.push(service_obj.port);
|
||||||
@@ -167,15 +167,16 @@ function configureService(service_name, service_obj, initial = false) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let outstr = '';
|
||||||
if ((service_name == "wtv-star" && self.no_star_word != true) || service_name != "wtv-star") {
|
if ((service_name == "wtv-star" && self.no_star_word != true) || service_name != "wtv-star") {
|
||||||
var outstr = "wtv-service: name=" + self.service_name + " host=" + self.host + " port=" + self.port;
|
outstr = `wtv-service: name=${self.service_name} host=${self.host} port=${self.port}`;
|
||||||
if (self.flags) outstr += " flags=" + self.flags;
|
if (self.flags) outstr += ` flags=${self.flags}`;
|
||||||
if (self.connections) outstr += " connections=" + self.connections;
|
if (self.connections) outstr += ` connections=${self.connections}`;
|
||||||
}
|
}
|
||||||
if (service_name == "wtv-star") {
|
if (service_name == "wtv-star") {
|
||||||
outstr += "\nwtv-service: name=wtv-* host=" + self.host + " port=" + self.port;
|
outstr += `\nwtv-service: name=wtv-* host=${self.host} port=${self.port}`;
|
||||||
if (self.flags) outstr += " flags=" + self.flags;
|
if (self.flags) outstr += ` flags=${self.flags}`;
|
||||||
if (self.connections) outstr += " connections=" + self.connections;
|
if (self.connections) outstr += ` connections=${self.connections}`;
|
||||||
}
|
}
|
||||||
return outstr;
|
return outstr;
|
||||||
}
|
}
|
||||||
@@ -184,8 +185,8 @@ function configureService(service_name, service_obj, initial = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Where we store our session information
|
// Where we store our session information
|
||||||
var ssid_sessions = new Array();
|
var ssid_sessions = [];
|
||||||
var socket_sessions = new Array();
|
var socket_sessions = [];
|
||||||
|
|
||||||
var ports = [];
|
var ports = [];
|
||||||
var pc_ports = [];
|
var pc_ports = [];
|
||||||
@@ -362,7 +363,7 @@ async function handleCGI(executable, cgi_file, socket, request_headers, vault, s
|
|||||||
}
|
}
|
||||||
var env = wtvshared.cloneObj(process.env);
|
var env = wtvshared.cloneObj(process.env);
|
||||||
env.QUERY_STRING = "";
|
env.QUERY_STRING = "";
|
||||||
var request_data = new Array();
|
var request_data = [];
|
||||||
var split_req = request_headers.request.split(' ');
|
var split_req = request_headers.request.split(' ');
|
||||||
request_data.method = split_req[0];
|
request_data.method = split_req[0];
|
||||||
var request_type = (request_headers.request_url.indexOf(":/")) ? request_headers.request_url.split(":/")[0] : 'http';
|
var request_type = (request_headers.request_url.indexOf(":/")) ? request_headers.request_url.split(":/")[0] : 'http';
|
||||||
@@ -485,7 +486,7 @@ async function handlePHP(socket, request_headers, php_file, vault, service_name,
|
|||||||
await handleCGI(minisrv_config.config.php_binpath, php_file, socket, request_headers, vault, service_name, session_data, extra_path);
|
await handleCGI(minisrv_config.config.php_binpath, php_file, socket, request_headers, vault, service_name, session_data, extra_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processPath(socket, service_vault_file_path, request_headers = new Array(), service_name, shared_romcache = null, pc_services = false) {
|
async function processPath(socket, service_vault_file_path, request_headers = [], service_name, shared_romcache = null, pc_services = false) {
|
||||||
var headers, data = null;
|
var headers, data = null;
|
||||||
var request_is_async = false;
|
var request_is_async = false;
|
||||||
var service_vault_found = false;
|
var service_vault_found = false;
|
||||||
@@ -535,7 +536,7 @@ async function processPath(socket, service_vault_file_path, request_headers = ne
|
|||||||
if (service_vault_found) return;
|
if (service_vault_found) return;
|
||||||
if (!usingSharedROMCache) {
|
if (!usingSharedROMCache) {
|
||||||
if (minisrv_config.config.SharedROMCache && shared_romcache) {
|
if (minisrv_config.config.SharedROMCache && shared_romcache) {
|
||||||
if (shared_romcache.indexOf(minisrv_config.config.SharedROMCache) != -1) {
|
if (shared_romcache.includes(minisrv_config.config.SharedROMCache)) {
|
||||||
var service_path_presplit = shared_romcache.split(path.sep);
|
var service_path_presplit = shared_romcache.split(path.sep);
|
||||||
service_path_presplit.splice(service_path_presplit.findIndex((element) => element === 'ROMCache'), 1);
|
service_path_presplit.splice(service_path_presplit.findIndex((element) => element === 'ROMCache'), 1);
|
||||||
var service_path_romcache = service_vault_dir + path.sep + service_path_presplit.join(path.sep);
|
var service_path_romcache = service_vault_dir + path.sep + service_path_presplit.join(path.sep);
|
||||||
@@ -575,7 +576,8 @@ async function processPath(socket, service_vault_file_path, request_headers = ne
|
|||||||
}
|
}
|
||||||
var is_dir = false;
|
var is_dir = false;
|
||||||
var file_exists = false;
|
var file_exists = false;
|
||||||
minisrv_catchall, service_path_split, service_path_request_file = null;
|
// Clear variables to free memory
|
||||||
|
minisrv_catchall = service_path_split = service_path_request_file = null;
|
||||||
if (fs.existsSync(service_vault_file_path)) {
|
if (fs.existsSync(service_vault_file_path)) {
|
||||||
file_exists = true;
|
file_exists = true;
|
||||||
is_dir = fs.lstatSync(service_vault_file_path).isDirectory()
|
is_dir = fs.lstatSync(service_vault_file_path).isDirectory()
|
||||||
@@ -629,7 +631,20 @@ async function processPath(socket, service_vault_file_path, request_headers = ne
|
|||||||
// Here we read back certain data from the ServiceVault Script Context Object
|
// Here we read back certain data from the ServiceVault Script Context Object
|
||||||
updateFromVM.forEach((item) => {
|
updateFromVM.forEach((item) => {
|
||||||
try {
|
try {
|
||||||
if (typeof vmResults[item[1]] !== "undefined") eval(item[0] + ' = vmResults["' + item[1] + '"]');
|
if (typeof vmResults[item[1]] !== "undefined") {
|
||||||
|
// Safely assign without eval
|
||||||
|
if (item[0] === `ssid_sessions['${socket.ssid}']` && privileged) {
|
||||||
|
ssid_sessions[socket.ssid] = vmResults[item[1]];
|
||||||
|
} else if (item[0] === 'headers') {
|
||||||
|
headers = vmResults[item[1]];
|
||||||
|
} else if (item[0] === 'data') {
|
||||||
|
data = vmResults[item[1]];
|
||||||
|
} else if (item[0] === 'request_is_async') {
|
||||||
|
request_is_async = vmResults[item[1]];
|
||||||
|
} else if (item[0] === 'socket_sessions' && privileged) {
|
||||||
|
socket_sessions = vmResults[item[1]];
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("vm readback error", e, item[0] + ' = vmResults[' + item[1] + ']');
|
console.error("vm readback error", e, item[0] + ' = vmResults[' + item[1] + ']');
|
||||||
}
|
}
|
||||||
@@ -800,9 +815,23 @@ async function processPath(socket, service_vault_file_path, request_headers = ne
|
|||||||
updateFromVM.forEach((item) => {
|
updateFromVM.forEach((item) => {
|
||||||
// Here we read back certain data from the ServiceVault Script Context Object
|
// Here we read back certain data from the ServiceVault Script Context Object
|
||||||
try {
|
try {
|
||||||
if (typeof vmResults[item[1]] !== "undefined") eval(item[0] + ' = vmResults["' + item[1] + '"]');
|
if (typeof vmResults[item[1]] !== "undefined") {
|
||||||
|
console.log(item[0])
|
||||||
|
// Safely assign without eval
|
||||||
|
if (item[0] === `ssid_sessions['${socket.ssid}']` && privileged) {
|
||||||
|
ssid_sessions[socket.ssid] = vmResults[item[1]];
|
||||||
|
} else if (item[0] === 'headers') {
|
||||||
|
headers = vmResults[item[1]];
|
||||||
|
} else if (item[0] === 'data') {
|
||||||
|
data = vmResults[item[1]];
|
||||||
|
} else if (item[0] === 'request_is_async') {
|
||||||
|
request_is_async = vmResults[item[1]];
|
||||||
|
} else if (item[0] === 'socket_sessions' && privileged) {
|
||||||
|
socket_sessions = vmResults[item[1]];
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("vm readback error", e);
|
console.error("vm readback error", e, item[0] + ' = vmResults[' + item[1] + ']');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (catchall_file.endsWith(".php")) {
|
} else if (catchall_file.endsWith(".php")) {
|
||||||
@@ -885,43 +914,38 @@ function getDirectoryIndex(svpath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function processURL(socket, request_headers, pc_services = false) {
|
async function processURL(socket, request_headers, pc_services = false) {
|
||||||
var shortURL, headers, data, service_name, original_service_name = "";
|
var shortURL, headers, data, service_name;
|
||||||
var allow_double_slash, enable_multi_query, use_external_proxy = false;
|
var original_service_name = "";
|
||||||
|
var allow_double_slash = false, enable_multi_query = false, use_external_proxy = false;
|
||||||
request_headers.query = {};
|
request_headers.query = {};
|
||||||
|
|
||||||
if (request_headers.request_url) {
|
if (request_headers.request_url) {
|
||||||
service_name = socket.service_name;
|
service_name = socket.service_name;
|
||||||
if (pc_services) {
|
if (pc_services) {
|
||||||
original_service_name = socket.service_name; // store service name
|
original_service_name = socket.service_name; // store service name
|
||||||
service_name = verifyServicePort(socket.service_name, socket); // get the actual ServiceVault path
|
service_name = verifyServicePort(socket.service_name, socket); // get the actual ServiceVault path
|
||||||
}
|
}
|
||||||
if (request_headers.request_url.indexOf('?') >= 0) {
|
if (request_headers.request_url.includes('?')) {
|
||||||
shortURL = request_headers.request_url.split('?')[0];
|
shortURL = request_headers.request_url.split('?')[0];
|
||||||
|
const qraw = request_headers.request_url.split('?')[1];
|
||||||
if (request_headers.request_url.indexOf('?') >= 0) {
|
if (qraw.length > 0) {
|
||||||
shortURL = request_headers.request_url.split('?')[0];
|
qraw.split("&").forEach(param => {
|
||||||
var qraw = request_headers.request_url.split('?')[1];
|
const qraw_split = param.split("=");
|
||||||
if (qraw.length > 0) {
|
if (qraw_split.length == 2) {
|
||||||
qraw = qraw.split("&");
|
const k = qraw_split[0];
|
||||||
for (let i = 0; i < qraw.length; i++) {
|
const value = unescape(qraw_split[1].replace(/\+/g, "%20"));
|
||||||
var qraw_split = qraw[i].split("=");
|
if (request_headers.query[k] && enable_multi_query) {
|
||||||
if (qraw_split.length == 2) {
|
if (typeof request_headers.query[k] === 'string') {
|
||||||
var k = qraw_split[0];
|
request_headers.query[k] = [request_headers.query[k]];
|
||||||
if (request_headers.query[k] && enable_multi_query) {
|
|
||||||
if (typeof request_headers.query[k] === 'string') {
|
|
||||||
var keyarray = [request_headers.query[k]];
|
|
||||||
request_headers.query[k] = keyarray;
|
|
||||||
}
|
|
||||||
request_headers.query[k].push(unescape(qraw[i].split("=")[1].replace(/\+/g, "%20")));
|
|
||||||
} else {
|
|
||||||
request_headers.query[k] = unescape(qraw[i].split("=")[1].replace(/\+/g, "%20"));
|
|
||||||
}
|
}
|
||||||
} else if (qraw[i].length == 1) {
|
request_headers.query[k].push(value);
|
||||||
request_headers.query[qraw[i]] = null;
|
} else {
|
||||||
|
request_headers.query[k] = value;
|
||||||
}
|
}
|
||||||
|
} else if (param.length == 1) {
|
||||||
|
request_headers.query[param] = null;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
} else {
|
|
||||||
shortURL = unescape(request_headers.request_url);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
shortURL = unescape(request_headers.request_url);
|
shortURL = unescape(request_headers.request_url);
|
||||||
@@ -1088,7 +1112,7 @@ minisrv-no-mail-count: true`;
|
|||||||
}
|
}
|
||||||
var urlToPath = wtvshared.fixPathSlashes(service_name + path.sep + shortURL.split(':/')[1]);
|
var urlToPath = wtvshared.fixPathSlashes(service_name + path.sep + shortURL.split(':/')[1]);
|
||||||
var shared_romcache = null;
|
var shared_romcache = null;
|
||||||
if ((shortURL.indexOf(":/ROMCache/") != -1 || shortURL.indexOf("://ROMCache/") != -1) && minisrv_config.config.enable_shared_romcache) {
|
if ((shortURL.includes(":/ROMCache/") || shortURL.includes("://ROMCache/")) && minisrv_config.config.enable_shared_romcache) {
|
||||||
shared_romcache = wtvshared.fixPathSlashes(minisrv_config.config.SharedROMCache + path.sep + shortURL.split(':/')[1]);
|
shared_romcache = wtvshared.fixPathSlashes(minisrv_config.config.SharedROMCache + path.sep + shortURL.split(':/')[1]);
|
||||||
}
|
}
|
||||||
if (minisrv_config.config.debug_flags.show_headers) console.debug(" * Incoming", (pc_services) ? "HTTP" : "WTVP", "headers on", (pc_services) ? "HTTP" : "WTVP", "socket ID", socket.id, await wtvshared.decodePostData(await wtvshared.filterRequestLog(await wtvshared.filterSSID(request_headers))));
|
if (minisrv_config.config.debug_flags.show_headers) console.debug(" * Incoming", (pc_services) ? "HTTP" : "WTVP", "headers on", (pc_services) ? "HTTP" : "WTVP", "socket ID", socket.id, await wtvshared.decodePostData(await wtvshared.filterRequestLog(await wtvshared.filterSSID(request_headers))));
|
||||||
@@ -1198,7 +1222,7 @@ function handleProxy(socket, request_type, request_headers, res, data) {
|
|||||||
|
|
||||||
// if Connection: close header, set our internal variable to close the socket
|
// if Connection: close header, set our internal variable to close the socket
|
||||||
if (headers['Connection']) {
|
if (headers['Connection']) {
|
||||||
if (headers['Connection'].toLowerCase().indexOf('close') !== -1) {
|
if (headers['Connection'].toLowerCase().includes('close')) {
|
||||||
headers["wtv-connection-close"] = true;
|
headers["wtv-connection-close"] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1235,7 +1259,7 @@ async function doHTTPProxy(socket, request_headers) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var request_data = new Array();
|
var request_data = [];
|
||||||
request_data.method = request_headers.request.split(' ')[0];
|
request_data.method = request_headers.request.split(' ')[0];
|
||||||
var request_url_split = request_headers.request.split(' ')[1].split('/');
|
var request_url_split = request_headers.request.split(' ')[1].split('/');
|
||||||
request_data.host = request_url_split[2];
|
request_data.host = request_url_split[2];
|
||||||
@@ -1690,7 +1714,7 @@ async function sendToSocket(socket, data) {
|
|||||||
if (socket_sessions[socket.id].post_data_length) delete socket_sessions[socket.id].post_data_length;
|
if (socket_sessions[socket.id].post_data_length) delete socket_sessions[socket.id].post_data_length;
|
||||||
if (socket_sessions[socket.id].post_data_percents_shown) delete socket_sessions[socket.id].post_data_percents_shown;
|
if (socket_sessions[socket.id].post_data_percents_shown) delete socket_sessions[socket.id].post_data_percents_shown;
|
||||||
socket.setTimeout(minisrv_config.config.socket_timeout * 1000);
|
socket.setTimeout(minisrv_config.config.socket_timeout * 1000);
|
||||||
if (socket_sessions[socket.id].close_me) socket.end();
|
if (socket_sessions[socket.id] && socket_sessions[socket.id].close_me) socket.end();
|
||||||
if (socket_sessions[socket.id].destroy_me) socket.destroy();
|
if (socket_sessions[socket.id].destroy_me) socket.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -469,7 +469,9 @@ class WTVClientSessionData {
|
|||||||
"contentType": contentType
|
"contentType": contentType
|
||||||
}));
|
}));
|
||||||
return true;
|
return true;
|
||||||
} catch {}
|
} catch (e) {
|
||||||
|
console.error("Error in addToScrapbook:", e);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ class WTVIRC {
|
|||||||
this.maxtargets = this.irc_config.max_targets || 4;
|
this.maxtargets = this.irc_config.max_targets || 4;
|
||||||
this.socket_timeout = 75; // Default socket timeout to 75 seconds, most clients will send PINGs every 60 seconds, so this should be enough to catch lost connections
|
this.socket_timeout = 75; // Default socket timeout to 75 seconds, most clients will send PINGs every 60 seconds, so this should be enough to catch lost connections
|
||||||
this.server_hello = this.irc_config.server_hello || `zefIRCd v${this.version} IRC server powered by minisrv`;
|
this.server_hello = this.irc_config.server_hello || `zefIRCd v${this.version} IRC server powered by minisrv`;
|
||||||
this.enable_eval = this.debug || false; // Enable eval in debug mode only
|
|
||||||
this.serverId = this.irc_config.server_id || '00A'; // Default server ID, can be overridden in config
|
this.serverId = this.irc_config.server_id || '00A'; // Default server ID, can be overridden in config
|
||||||
this.allow_public_vhosts = this.irc_config.allow_public_vhosts || true; // If true, users can set their host to a virtual host that is not a real hostname or IP address, if false, only opers can.
|
this.allow_public_vhosts = this.irc_config.allow_public_vhosts || true; // If true, users can set their host to a virtual host that is not a real hostname or IP address, if false, only opers can.
|
||||||
this.kick_insecure_users_on_secure = this.irc_config.kick_insecure_users_on_secure || true; // If true, users without SSL connections will be kicked from a channel when +Z is applied
|
this.kick_insecure_users_on_secure = this.irc_config.kick_insecure_users_on_secure || true; // If true, users without SSL connections will be kicked from a channel when +Z is applied
|
||||||
@@ -2796,27 +2795,6 @@ class WTVIRC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'EVAL':
|
|
||||||
// VERY DANGEROUS
|
|
||||||
if (!this.checkRegistered(socket)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (!this.isIRCOp(socket.nickname)) {
|
|
||||||
await this.safeWriteToSocket(socket, `:${this.servername} 481 ${socket.nickname} :Permission denied - you are not an IRC operator\r\n`);
|
|
||||||
this.debugLog('warn', `EVAL command attempted by non-IRCOp: ${socket.nickname}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (!this.enable_eval) {
|
|
||||||
await this.safeWriteToSocket(socket, `:${this.servername} 404 ${socket.nickname} :Eval is disabled\r\n`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const result = eval(params.join(' '));
|
|
||||||
await this.safeWriteToSocket(socket, `:${this.servername} 200 ${socket.nickname} :${result}\r\n`);
|
|
||||||
} catch (error) {
|
|
||||||
await this.safeWriteToSocket(socket, `:${this.servername} 500 ${socket.nickname} :Error evaluating expression\r\n`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'KILL':
|
case 'KILL':
|
||||||
if (!this.checkRegistered(socket)) {
|
if (!this.checkRegistered(socket)) {
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user