mirror of https://github.com/calzoneman/sync.git
Improve strictness of data checking
This commit is contained in:
parent
51d89b99e8
commit
2744b3a20d
|
@ -1,3 +1,8 @@
|
|||
Sat Oct 12 18:58 2013 CDT
|
||||
* lib/user.js, lib/channel.js: Improve strictness of data checking
|
||||
to prevent errors from incoming bad data. Kick users who send
|
||||
bad data or attempt channel moderation with insufficient rank.
|
||||
|
||||
Sat Oct 12 18:24 2013 CDT
|
||||
* lib/user.js: Fix bad chatMsg packet causing exceptions
|
||||
|
||||
|
|
|
@ -362,8 +362,10 @@ Channel.prototype.readLog = function (filterIp, callback) {
|
|||
}
|
||||
|
||||
Channel.prototype.tryReadLog = function (user) {
|
||||
if(user.rank < 3)
|
||||
if(user.rank < 3) {
|
||||
user.kick("Attempted readChanLog with insufficient permission");
|
||||
return;
|
||||
}
|
||||
|
||||
var filterIp = true;
|
||||
if(user.global_rank >= 255)
|
||||
|
@ -623,6 +625,7 @@ Channel.prototype.tryNameBan = function(actor, name) {
|
|||
Channel.prototype.unbanName = function(actor, name) {
|
||||
var self = this;
|
||||
if(!self.hasPermission(actor, "ban")) {
|
||||
actor.kick("Attempted unban with insufficient permission");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -730,8 +733,10 @@ Channel.prototype.tryIPBan = function(actor, name, range) {
|
|||
|
||||
Channel.prototype.unbanIP = function(actor, ip) {
|
||||
var self = this;
|
||||
if(!self.hasPermission(actor, "ban"))
|
||||
if(!self.hasPermission(actor, "ban")) {
|
||||
actor.kick("Attempted unban with insufficient permission");
|
||||
return false;
|
||||
}
|
||||
|
||||
self.ipbans[ip] = null;
|
||||
self.users.forEach(function(u) {
|
||||
|
@ -748,11 +753,11 @@ Channel.prototype.unbanIP = function(actor, ip) {
|
|||
}
|
||||
|
||||
Channel.prototype.tryUnban = function(actor, data) {
|
||||
if(data.ip_hidden) {
|
||||
if(typeof data.ip_hidden === "string") {
|
||||
var ip = this.hideIP(data.ip_hidden);
|
||||
this.unbanIP(actor, ip);
|
||||
}
|
||||
if(data.name) {
|
||||
if(typeof data.name === "string") {
|
||||
this.unbanName(actor, data.name);
|
||||
}
|
||||
}
|
||||
|
@ -1318,9 +1323,11 @@ Channel.prototype.tryQueue = function(user, data) {
|
|||
return;
|
||||
}
|
||||
if(typeof data.pos !== "string") {
|
||||
user.kick("Bad queue packet");
|
||||
return;
|
||||
}
|
||||
if(typeof data.id !== "string" && data.id !== false) {
|
||||
user.kick("Bad queue packet");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1580,6 +1587,7 @@ Channel.prototype.tryQueuePlaylist = function(user, data) {
|
|||
|
||||
if(typeof data.name !== "string" ||
|
||||
typeof data.pos !== "string") {
|
||||
user.kick("Bad queuePlaylist packet");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1639,6 +1647,7 @@ Channel.prototype.trySetTemp = function(user, data) {
|
|||
return;
|
||||
}
|
||||
if(typeof data.uid != "number" || typeof data.temp != "boolean") {
|
||||
user.kick("Bad setTemp packet");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1667,8 +1676,10 @@ Channel.prototype.tryDequeue = function(user, data) {
|
|||
if(!this.hasPermission(user, "playlistdelete"))
|
||||
return;
|
||||
|
||||
if(typeof data !== "number")
|
||||
if(typeof data !== "number") {
|
||||
user.kick("Bad delete packet");
|
||||
return;
|
||||
}
|
||||
|
||||
var plitem = this.playlist.items.find(data);
|
||||
if(plitem && plitem.media)
|
||||
|
@ -1682,6 +1693,7 @@ Channel.prototype.tryUncache = function(user, data) {
|
|||
return;
|
||||
}
|
||||
if(typeof data.id != "string") {
|
||||
user.kick("Bad uncache packet");
|
||||
return;
|
||||
}
|
||||
if (!self.registered)
|
||||
|
@ -1723,6 +1735,7 @@ Channel.prototype.tryJumpTo = function(user, data) {
|
|||
}
|
||||
|
||||
if(typeof data !== "number") {
|
||||
user.kick("Bad jumpTo packet");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1776,11 +1789,14 @@ Channel.prototype.tryShufflequeue = function(user) {
|
|||
|
||||
Channel.prototype.tryUpdate = function(user, data) {
|
||||
if(this.leader != user) {
|
||||
user.kick("Received mediaUpdate from non-leader");
|
||||
return;
|
||||
}
|
||||
|
||||
if(typeof data.id !== "string" || typeof data.currentTime !== "number")
|
||||
return;
|
||||
if(typeof data.id !== "string" || typeof data.currentTime !== "number") {
|
||||
user.kick("Bad mediaUpdate packet");
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.playlist.current === null) {
|
||||
return;
|
||||
|
@ -1832,8 +1848,10 @@ Channel.prototype.tryMove = function(user, data) {
|
|||
return;
|
||||
}
|
||||
|
||||
if(typeof data.from !== "number" || (typeof data.after !== "number" && typeof data.after !== "string"))
|
||||
if(typeof data.from !== "number" || (typeof data.after !== "number" && typeof data.after !== "string")) {
|
||||
user.kick("Bad moveMedia packet");
|
||||
return;
|
||||
}
|
||||
|
||||
this.move(data, user);
|
||||
}
|
||||
|
@ -1845,7 +1863,8 @@ Channel.prototype.tryOpenPoll = function(user, data) {
|
|||
return;
|
||||
}
|
||||
|
||||
if(!data.title || !data.opts) {
|
||||
if(typeof data.title !== "string" || !(data.opts instanceof Array)) {
|
||||
user.kick("Invalid newPoll packet");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1878,6 +1897,7 @@ Channel.prototype.tryVote = function(user, data) {
|
|||
return;
|
||||
}
|
||||
if(typeof data.option !== "number") {
|
||||
user.kick("Bad vote packet");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1952,8 +1972,10 @@ Channel.prototype.tryToggleLock = function(user) {
|
|||
}
|
||||
|
||||
Channel.prototype.tryRemoveFilter = function(user, f) {
|
||||
if(!this.hasPermission(user, "filteredit"))
|
||||
return false;
|
||||
if(!this.hasPermission(user, "filteredit")) {
|
||||
user.kick("Attempted removeFilter with insufficient permission");
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log("%%% " + user.name + " removed filter: " + f.name);
|
||||
this.removeFilter(f);
|
||||
|
@ -1989,6 +2011,13 @@ Channel.prototype.updateFilter = function(filter, emit) {
|
|||
|
||||
Channel.prototype.tryUpdateFilter = function(user, f) {
|
||||
if(!this.hasPermission(user, "filteredit")) {
|
||||
user.kick("Attempted updateFilter with insufficient permission");
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof f.source !== "string" || typeof f.flags !== "string" ||
|
||||
typeof f.replace !== "string") {
|
||||
user.kick("Bad updateFilter packet");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2006,8 +2035,8 @@ Channel.prototype.tryUpdateFilter = function(user, f) {
|
|||
return;
|
||||
}
|
||||
var filter = new Filter(f.name, f.source, f.flags, f.replace);
|
||||
filter.active = f.active;
|
||||
filter.filterlinks = f.filterlinks;
|
||||
filter.active = !!f.active;
|
||||
filter.filterlinks = !!f.filterlinks;
|
||||
this.logger.log("%%% " + user.name + " updated filter: " + f.name);
|
||||
this.updateFilter(filter);
|
||||
}
|
||||
|
@ -2026,16 +2055,21 @@ Channel.prototype.moveFilter = function(data) {
|
|||
}
|
||||
|
||||
Channel.prototype.tryMoveFilter = function(user, data) {
|
||||
if(!this.hasPermission(user, "filteredit"))
|
||||
if(!this.hasPermission(user, "filteredit")) {
|
||||
user.kick("Attempted moveFilter with insufficient permission");
|
||||
return;
|
||||
}
|
||||
|
||||
if(typeof data.to !== "number" || typeof data.from !== "number")
|
||||
if(typeof data.to !== "number" || typeof data.from !== "number") {
|
||||
user.kick("Bad moveFilter packet");
|
||||
return;
|
||||
}
|
||||
this.moveFilter(data);
|
||||
}
|
||||
|
||||
Channel.prototype.tryUpdatePermissions = function(user, perms) {
|
||||
if(user.rank < 3) {
|
||||
user.kick("Attempted setPermissions with insufficient permission");
|
||||
return;
|
||||
}
|
||||
for(var key in perms) {
|
||||
|
@ -2047,6 +2081,7 @@ Channel.prototype.tryUpdatePermissions = function(user, perms) {
|
|||
|
||||
Channel.prototype.tryUpdateOptions = function(user, data) {
|
||||
if(user.rank < 2) {
|
||||
user.kick("Attempted setOptions with insufficient permission");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2083,9 +2118,14 @@ Channel.prototype.tryUpdateOptions = function(user, data) {
|
|||
|
||||
Channel.prototype.trySetCSS = function(user, data) {
|
||||
if(user.rank < 3) {
|
||||
user.kick("Attempted setChannelCSS with insufficient permission");
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data.css !== "string") {
|
||||
user.kick("Bad setChannelCSS packet");
|
||||
return;
|
||||
}
|
||||
var css = data.css || "";
|
||||
if(css.length > 20000) {
|
||||
css = css.substring(0, 20000);
|
||||
|
@ -2100,6 +2140,11 @@ Channel.prototype.trySetCSS = function(user, data) {
|
|||
|
||||
Channel.prototype.trySetJS = function(user, data) {
|
||||
if(user.rank < 3) {
|
||||
user.kick("Attempted setChannelJS with insufficient permission");
|
||||
return;
|
||||
}
|
||||
if (typeof data.js !== "string") {
|
||||
user.kick("Bad setChannelJS packet");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2131,6 +2176,12 @@ Channel.prototype.updateMotd = function(motd) {
|
|||
|
||||
Channel.prototype.tryUpdateMotd = function(user, data) {
|
||||
if(!this.hasPermission(user, "motdedit")) {
|
||||
user.kick("Attempted setMotd with insufficient permission");
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data.motd !== "string") {
|
||||
user.kick("Bad setMotd packet");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2241,11 +2292,15 @@ Channel.prototype.sendMessage = function(username, msg, msgclass, data) {
|
|||
|
||||
Channel.prototype.trySetRank = function(user, data) {
|
||||
var self = this;
|
||||
if(user.rank < 2)
|
||||
if(user.rank < 2) {
|
||||
user.kick("Attempted setChannelRank with insufficient permission");
|
||||
return;
|
||||
}
|
||||
|
||||
if(typeof data.user !== "string" || typeof data.rank !== "number")
|
||||
if(typeof data.user !== "string" || typeof data.rank !== "number") {
|
||||
user.kick("Bad setChannelRank packet");
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.rank >= user.rank)
|
||||
return;
|
||||
|
@ -2337,10 +2392,12 @@ Channel.prototype.changeLeader = function(name) {
|
|||
|
||||
Channel.prototype.tryChangeLeader = function(user, data) {
|
||||
if(user.rank < 2) {
|
||||
user.kick("Attempted assignLeader with insufficient permission");
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.name == undefined) {
|
||||
if(typeof data.name !== "string") {
|
||||
user.kick("Bad assignLeader packet");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
90
lib/user.js
90
lib/user.js
|
@ -119,6 +119,11 @@ User.prototype.autoAFK = function () {
|
|||
}, self.channel.opts.afk_timeout * 1000);
|
||||
};
|
||||
|
||||
User.prototype.kick = function (reason) {
|
||||
this.socket.emit("kick", { reason: reason });
|
||||
this.socket.disconnect(true);
|
||||
};
|
||||
|
||||
User.prototype.initCallbacks = function () {
|
||||
var self = this;
|
||||
self.socket.on("disconnect", function () {
|
||||
|
@ -128,19 +133,19 @@ User.prototype.initCallbacks = function () {
|
|||
});
|
||||
|
||||
self.socket.on("joinChannel", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel())
|
||||
return;
|
||||
if (typeof data.name != "string")
|
||||
if (typeof data.name != "string") {
|
||||
self.kick("Bad joinChannel packet");
|
||||
return;
|
||||
}
|
||||
if (!data.name.match(/^[\w-_]{1,30}$/)) {
|
||||
self.socket.emit("errorMsg", {
|
||||
msg: "Invalid channel name. Channel names may consist of"+
|
||||
" 1-30 characters in the set a-z, A-Z, 0-9, -, and _"
|
||||
});
|
||||
self.socket.emit("kick", {
|
||||
reason: "Bad channel name"
|
||||
});
|
||||
|
||||
self.kick("Invalid channel name");
|
||||
return;
|
||||
}
|
||||
data.name = data.name.toLowerCase();
|
||||
|
@ -156,9 +161,10 @@ User.prototype.initCallbacks = function () {
|
|||
});
|
||||
|
||||
self.socket.on("login", function (data) {
|
||||
var name = data.name || "";
|
||||
var pw = data.pw || "";
|
||||
var session = data.session || "";
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
var name = (typeof data.name === "string") ? data.name : "";
|
||||
var pw = (typeof data.pw === "string") ? data.pw : "";
|
||||
var session = (typeof data.session === "string") ? data.session : "";
|
||||
if (pw.length > 100)
|
||||
pw = pw.substring(0, 100);
|
||||
|
||||
|
@ -186,52 +192,32 @@ User.prototype.initCallbacks = function () {
|
|||
});
|
||||
|
||||
self.socket.on("assignLeader", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryChangeLeader(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("promote", function (data) {
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryPromoteUser(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("demote", function (data) {
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryDemoteUser(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("setChannelRank", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.trySetRank(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("banName", function (data) {
|
||||
if (self.inChannel()) {
|
||||
self.channel.banName(self, data.name || "");
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("banIP", function (data) {
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryIPBan(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("unban", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryUnban(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("chatMsg", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
if (typeof data.msg !== "string") {
|
||||
self.socket.emit("kick", {
|
||||
reason: "Invalid chatMsg packet!"
|
||||
reason: "Invalid chatMsg packet"
|
||||
});
|
||||
self.socket.disconnect(true);
|
||||
return;
|
||||
|
@ -245,6 +231,7 @@ User.prototype.initCallbacks = function () {
|
|||
});
|
||||
|
||||
self.socket.on("newPoll", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryOpenPoll(self, data);
|
||||
}
|
||||
|
@ -263,36 +250,42 @@ User.prototype.initCallbacks = function () {
|
|||
});
|
||||
|
||||
self.socket.on("queue", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryQueue(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("setTemp", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.trySetTemp(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("delete", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryDequeue(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("uncache", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryUncache(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("moveMedia", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryMove(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("jumpTo", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryJumpTo(self, data);
|
||||
}
|
||||
|
@ -323,14 +316,20 @@ User.prototype.initCallbacks = function () {
|
|||
});
|
||||
|
||||
self.socket.on("mediaUpdate", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryUpdate(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("searchMedia", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
if (data.source == "yt") {
|
||||
if (typeof data.query !== "string") {
|
||||
self.kick("Bad searchMedia packet");
|
||||
return;
|
||||
}
|
||||
if (data.source === "yt") {
|
||||
var searchfn = InfoGetter.Getters.ytSearch;
|
||||
searchfn(data.query.split(" "), function (e, vids) {
|
||||
if (!e) {
|
||||
|
@ -358,6 +357,7 @@ User.prototype.initCallbacks = function () {
|
|||
});
|
||||
|
||||
self.socket.on("vote", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryVote(self, data);
|
||||
}
|
||||
|
@ -378,52 +378,64 @@ User.prototype.initCallbacks = function () {
|
|||
if (!self.inChannel()) {
|
||||
return;
|
||||
}
|
||||
if (self.rank < 10) {
|
||||
self.kick("Attempted unregisterChannel with insufficient permission");
|
||||
return;
|
||||
}
|
||||
self.channel.unregister(self);
|
||||
});
|
||||
|
||||
self.socket.on("setOptions", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryUpdateOptions(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("setPermissions", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryUpdatePermissions(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("setChannelCSS", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.trySetCSS(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("setChannelJS", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.trySetJS(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("updateFilter", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryUpdateFilter(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("removeFilter", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryRemoveFilter(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("moveFilter", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryMoveFilter(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("setMotd", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryUpdateMotd(self, data);
|
||||
}
|
||||
|
@ -456,12 +468,14 @@ User.prototype.initCallbacks = function () {
|
|||
});
|
||||
|
||||
self.socket.on("voteskip", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryVoteskip(self);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("listPlaylists", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.name === "" || self.rank < 1) {
|
||||
self.socket.emit("listPlaylists", {
|
||||
pllist: [],
|
||||
|
@ -483,6 +497,11 @@ User.prototype.initCallbacks = function () {
|
|||
});
|
||||
|
||||
self.socket.on("savePlaylist", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (typeof data.name !== "string") {
|
||||
self.kick("Bad savePlaylist packet");
|
||||
return;
|
||||
}
|
||||
if (self.rank < 1) {
|
||||
self.socket.emit("savePlaylist", {
|
||||
success: false,
|
||||
|
@ -533,13 +552,16 @@ User.prototype.initCallbacks = function () {
|
|||
});
|
||||
|
||||
self.socket.on("queuePlaylist", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (self.inChannel()) {
|
||||
self.channel.tryQueuePlaylist(self, data);
|
||||
}
|
||||
});
|
||||
|
||||
self.socket.on("deletePlaylist", function (data) {
|
||||
data = (typeof data !== "object") ? {} : data;
|
||||
if (typeof data.name != "string") {
|
||||
self.kick("Bad deletePlaylist packet");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue