started update for readme

start update for version changes
hopefully fixed pesky stat bug
move vpn detection into script plugin
This commit is contained in:
RaidMax 2018-08-26 19:20:47 -05:00
parent 1343d4959e
commit 0538d9f479
22 changed files with 388 additions and 192 deletions

View File

@ -415,6 +415,34 @@ namespace IW4MAdmin.Application
try
{
await newEvent.Owner.ExecuteEvent(newEvent);
// todo: this is a hacky mess
if (newEvent.Origin?.DelayedEvents?.Count > 0 &&
newEvent.Origin?.State == Player.ClientState.Connected)
{
var events = newEvent.Origin.DelayedEvents;
// add the delayed event to the queue
while (events?.Count > 0)
{
var e = events.Dequeue();
e.Origin = newEvent.Origin;
// check if the target was assigned
if (e.Target != null)
{
// update the target incase they left or have newer info
e.Target = newEvent.Owner.GetPlayersAsList()
.FirstOrDefault(p => p.NetworkId == e.Target.NetworkId);
// we have to throw out the event because they left
if (e.Target == null)
{
Logger.WriteWarning($"Delayed event for {e.Origin} was removed because the target has left");
continue;
}
}
this.GetEventHandler().AddEvent(e);
}
}
#if DEBUG
Logger.WriteDebug("Processed Event");
#endif

View File

@ -1,42 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.Misc
{
public class VPNCheck
{
public static async Task<bool> UsingVPN(string ip, string apiKey)
{
#if DEBUG
return await Task.FromResult(false);
#else
try
{
using (var RequestClient = new System.Net.Http.HttpClient())
{
RequestClient.DefaultRequestHeaders.Add("X-Key", apiKey);
string response = await RequestClient.GetStringAsync($"http://v2.api.iphub.info/ip/{ip}");
var responseJson = JsonConvert.DeserializeObject<JObject>(response);
int blockType = Convert.ToInt32(responseJson["block"]);
/*if (responseJson.ContainsKey("isp"))
{
if (responseJson["isp"].ToString() == "TSF-IP-CORE")
return true;
}*/
return blockType == 1;
}
}
catch (Exception)
{
return false;
}
#endif
}
}
}

View File

@ -16,7 +16,6 @@ using SharedLibraryCore.Configuration;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Localization;
using IW4MAdmin.Application.Misc;
using IW4MAdmin.Application.RconParsers;
using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.IO;
@ -96,9 +95,6 @@ namespace IW4MAdmin
{
return true;
}
// if they're authenticated but haven't been added yet
// we want to set their delayed events
var delayedEventQueue = Players[polledPlayer.ClientNumber].DelayedEvents;
#if !DEBUG
if (polledPlayer.Name.Length < 3)
@ -253,14 +249,6 @@ namespace IW4MAdmin
return true;
}
if (!Manager.GetApplicationSettings().Configuration().EnableClientVPNs &&
Manager.GetApplicationSettings().Configuration().VpnExceptionIds?.FirstOrDefault(i => i == player.ClientId) != null &&
await VPNCheck.UsingVPN(player.IPAddressString, Manager.GetApplicationSettings().Configuration().IPHubAPIKey))
{
await player.Kick(Utilities.CurrentLocalization.LocalizationIndex["SERVER_KICK_VPNS_NOTALLOWED"], new Player() { ClientId = 1 });
return true;
}
var e = new GameEvent()
{
Type = GameEvent.EventType.Connect,
@ -271,28 +259,6 @@ namespace IW4MAdmin
Manager.GetEventHandler().AddEvent(e);
player.State = Player.ClientState.Connected;
// add the delayed event to the queue
while (delayedEventQueue?.Count > 0)
{
e = delayedEventQueue.Dequeue();
e.Origin = player;
// check if the target was assigned
if (e.Target != null)
{
// update the target incase they left or have newer info
e.Target = GetPlayersAsList()
.FirstOrDefault(p => p.NetworkId == e.Target.NetworkId);
// we have to throw out the event because they left
if (e.Target == null)
{
Logger.WriteWarning($"Delayed event for {e.Origin} was removed because the target has left");
continue;
}
}
Manager.GetEventHandler().AddEvent(e);
}
return true;
}
@ -353,24 +319,54 @@ namespace IW4MAdmin
}
}
// this allows us to catch exceptions but still run it parallel
async Task pluginHandlingAsync(Task onEvent, string pluginName)
//// this allows us to catch exceptions but still run it parallel
//async Task pluginHandlingAsync(Task onEvent, string pluginName)
//{
// try
// {
// await onEvent;
// }
// // this happens if a plugin (login) wants to stop commands from executing
// catch (AuthorizationException e)
// {
// await E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
// canExecuteCommand = false;
// }
// catch (Exception Except)
// {
// Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{pluginName}]");
// Logger.WriteDebug(String.Format("Error Message: {0}", Except.Message));
// Logger.WriteDebug(String.Format("Error Trace: {0}", Except.StackTrace));
// while (Except.InnerException != null)
// {
// Except = Except.InnerException;
// Logger.WriteDebug($"Inner exception: {Except.Message}");
// }
// }
//}
//var pluginTasks = SharedLibraryCore.Plugins.PluginImporter.ActivePlugins.
// Select(p => pluginHandlingAsync(p.OnEventAsync(E, this), p.Name));
//// execute all the plugin updates simultaneously
//await Task.WhenAll(pluginTasks);
foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
{
try
{
await onEvent;
await plugin.OnEventAsync(E, this);
}
// this happens if a plugin (login) wants to stop commands from executing
catch (AuthorizationException e)
{
await E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
canExecuteCommand = false;
}
catch (Exception Except)
{
Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{pluginName}]");
Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{plugin.Name}]");
Logger.WriteDebug(String.Format("Error Message: {0}", Except.Message));
Logger.WriteDebug(String.Format("Error Trace: {0}", Except.StackTrace));
while (Except.InnerException != null)
@ -381,12 +377,6 @@ namespace IW4MAdmin
}
}
var pluginTasks = SharedLibraryCore.Plugins.PluginImporter.ActivePlugins.
Select(p => pluginHandlingAsync(p.OnEventAsync(E, this), p.Name));
// execute all the plugin updates simultaneously
await Task.WhenAll(pluginTasks);
// hack: this prevents commands from getting executing that 'shouldn't' be
if (E.Type == GameEvent.EventType.Command &&
E.Extra != null &&
@ -405,9 +395,6 @@ namespace IW4MAdmin
override protected async Task ProcessEvent(GameEvent E)
{
if (E.Type == GameEvent.EventType.Connect)
{
// this may be a fix for a hard to reproduce null exception error
lock (ChatHistory)
{
ChatHistory.Add(new ChatInfo()
{
@ -415,7 +402,6 @@ namespace IW4MAdmin
Message = "CONNECTED",
Time = DateTime.UtcNow
});
}
if (E.Origin.Level > Player.Permission.Moderator)
await E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count));
@ -592,6 +578,8 @@ namespace IW4MAdmin
Owner = this
};
client.State = Player.ClientState.Disconnecting;
Manager.GetEventHandler().AddEvent(e);
// todo: needed?
// wait until the disconnect event is complete
@ -601,11 +589,10 @@ namespace IW4MAdmin
AuthQueue.AuthenticateClients(CurrentPlayers);
// all polled players should be authenticated
var addPlayerTasks = AuthQueue.GetAuthenticatedClients()
.Select(client => AddPlayer(client));
await Task.WhenAll(addPlayerTasks);
foreach (var c in AuthQueue.GetAuthenticatedClients())
{
await AddPlayer(c);
}
return CurrentPlayers.Count;
}

View File

@ -1,7 +1,7 @@
certifi==2018.4.16
chardet==3.0.4
idna==2.7
pip==18.0
requests==2.19.1
setuptools==39.0.1
urllib3==1.23
certifi>=2018.4.16
chardet>=3.0.4
idna>=2.7
pip>=18.0
requests>=2.19.1
setuptools>=39.0.1
urllib3>=1.23

View File

@ -35,6 +35,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IW4ScriptCommands", "Plugin
EndProject
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "DiscordWebhook", "DiscordWebhook\DiscordWebhook.pyproj", "{15A81D6E-7502-46CE-8530-0647A380B5F4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlugins", "{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA}"
ProjectSection(SolutionItems) = preProject
Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -312,6 +317,7 @@ Global
{D9F2ED28-6FA5-40CA-9912-E7A849147AB1} = {26E8B310-269E-46D4-A612-24601F16065F}
{B72DEBFB-9D48-4076-8FF5-1FD72A830845} = {26E8B310-269E-46D4-A612-24601F16065F}
{6C706CE5-A206-4E46-8712-F8C48D526091} = {26E8B310-269E-46D4-A612-24601F16065F}
{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA} = {26E8B310-269E-46D4-A612-24601F16065F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}

View File

@ -108,7 +108,6 @@
<Folder Include="master\templates\" />
</ItemGroup>
<ItemGroup>
<None Include="FolderProfile.pubxml" />
<Content Include="master\config\master.json" />
<Content Include="requirements.txt" />
<Content Include="master\templates\index.html" />

View File

@ -15,9 +15,9 @@ class Base():
self.scheduler.start()
self.scheduler.add_job(
func=self._remove_staleinstances,
trigger=IntervalTrigger(seconds=120),
trigger=IntervalTrigger(seconds=60),
id='stale_instance_remover',
name='Remove stale instances if no heartbeat in 120 seconds',
name='Remove stale instances if no heartbeat in 60 seconds',
replace_existing=True
)
self.scheduler.add_job(
@ -41,7 +41,7 @@ class Base():
def _remove_staleinstances(self):
for key, value in list(self.instance_list.items()):
if int(time.time()) - value.last_heartbeat > 120:
if int(time.time()) - value.last_heartbeat > 60:
print('[_remove_staleinstances] removing stale instance {id}'.format(id=key))
del self.instance_list[key]
del self.token_list[key]

View File

@ -11,7 +11,7 @@ class Authenticate(Resource):
if ctx.get_token(instance_id) is not False:
return { 'message' : 'that id already has a token'}, 401
else:
expires = datetime.timedelta(days=1)
expires = datetime.timedelta(days=30)
token = create_access_token(instance_id, expires_delta=expires)
ctx.add_token(instance_id, token)
return { 'access_token' : token }, 200

View File

@ -0,0 +1,63 @@
const plugin = {
author: 'RaidMax',
version: 1.0,
name: 'VPN Kick Plugin',
manager: null,
logger: null,
vpnExceptionIds: [],
checkForVpn(origin) {
let exempt = false;
// prevent players that are exempt from being kicked
this.vpnExceptionIds.forEach(function(id) {
if (id === origin.ClientId) {
exempt = true;
return false;
}
});
if (exempt) {
return;
}
let usingVPN = false;
try {
let httpRequest = System.Net.WebRequest.Create('https://api.xdefcon.com/proxy/check/?ip=' + origin.IPAddressString);
let response = httpRequest.GetResponse();
let data = response.GetResponseStream();
let streamReader = new System.IO.StreamReader(data);
let jsonResponse = streamReader.ReadToEnd();
streamReader.Dispose();
response.Close();
let parsedJSON = JSON.parse(jsonResponse);
usingVPN = parsedJSON['success'] && parsedJSON['proxy'];
} catch (e) {
this.logger.WriteError(e.message);
}
if (usingVPN) {
let library = importNamespace('SharedLibraryCore');
let kickOrigin = new library.Objects.Player();
kickOrigin.ClientId = 1;
origin.Kick(_localization.LocalizationIndex["SERVER_KICK_VPNS_NOTALLOWED"], kickOrigin);
}
},
onEventAsync(gameEvent, server) {
// connect event
if (gameEvent.Type === 3) {
this.checkForVpn(gameEvent.Origin)
}
},
onLoadAsync(manager) {
this.manager = manager;
this.logger = manager.GetLogger();
},
onUnloadAsync() {},
onTickAsync(server) {}
}

View File

@ -435,6 +435,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
case Penalty.PenaltyType.Ban:
if (attacker.Level == Player.Permission.Banned)
break;
await saveLog();
await attacker.Ban(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_CHEAT_DETECTED"], new Player()
{
ClientId = 1,
@ -448,7 +449,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
}
}
});
await saveLog();
break;
case Penalty.PenaltyType.Flag:
if (attacker.Level != Player.Permission.User)

View File

@ -66,7 +66,7 @@ namespace IW4MAdmin.Plugins.Stats.Web.Controllers
.Include(s => s.HitDestination)
.Include(s => s.CurrentViewAngle)
.Include(s => s.PredictedViewAngles)
.OrderBy(s => s.When)
.OrderBy(s => new { s.When, s.Hits })
.ToListAsync();
if (penaltyInfo != null)

194
README.md
View File

@ -1,19 +1,16 @@
# IW4MAdmin
### Quick Start Guide
### Version 2.1
### Version 2.2
_______
### About
**IW4MAdmin** is an administration tool for [IW4x](https://iw4xcachep26muba.onion.link/), [Pluto T6](https://forum.plutonium.pw/category/33/plutonium-t6), [Pluto IW5](https://forum.plutonium.pw/category/5/plutonium-iw5), and most Call of Duty® dedicated servers. It allows complete control of your server; from changing maps, to banning players, **IW4MAdmin** monitors and records activity on your server(s). With plugin support, extending its functionality is a breeze.
### Download
Latest binary builds are always available at https://raidmax.org/IW4MAdmin
### Setup
**IW4MAdmin** requires minimal configuration to run. There is only one prerequisite.
* [.NET Core 2.0.7 Runtime](https://www.microsoft.com/net/download/dotnet-core/runtime-2.0.7) *or newer*
* [.NET Core 2.1 Runtime](https://www.microsoft.com/net/download) *or newer*
1. Extract `IW4MAdmin-<version>.zip`
2. Open command prompt or terminal in the extracted folder
3. Run `dotnet IW4MAdmin.dll`
2. Run `StartIW4MAdmin.cmd`
___
### Configuration
@ -37,14 +34,33 @@ When **IW4MAdmin** is launched for the _first time_, you will be prompted to set
* Allow clients to use a [VPN](https://en.wikipedia.org/wiki/Virtual_private_network)
* _This feature requires an active api key on [iphub.info](https://iphub.info/)_
`Enable discord link`
* Shows a link to your server's discord on the webfront
* _This feature requires an invite link to your discord server_
`Enable social link`
* Shows a link to your community's social media/website on the webfront
`Use Custom Encoding Parser`
* Allows alternative encodings to be used for parsing game information and events
* **Russian users should use this and then specify** `windows-1251` **as the encoding string**
#### Server Configuration
After initial configuration is finished, you will be prompted to configure your servers for **IW4MAdmin**.
`Enter server IP Address`
* For almost all scenarios `127.0.0.1` is sufficient
`Enter server port`
* The port that your server is listening on (can be obtained via `net_port`)
`Enter server RCon password`
* The *\(R\)emote (Con)sole* password set in your server configuration (can be obtained via `rcon_password`)
`Use Pluto T6 parser`
* Used if setting up a server for Plutonium T6 (BO2)
`Use Pluto IW5 parser`
* Used if setting a server for Plutonium IW5 (MW3)
`Enter number of reserved slots`
* The number of client slots reserver for privileged players (unavailable for regular users to occupy)
#### Advanced Configuration
If you wish to further customize your experience of **IW4MAdmin**, the following configuration file(s) will allow you to changes core options using any text-editor.
@ -54,6 +70,20 @@ If you wish to further customize your experience of **IW4MAdmin**, the following
`WebfrontBindUrl`
* Specifies the address and port the webfront will listen on.
* The value can be an [IP Address](https://en.wikipedia.org/wiki/IP_address):port or [Domain Name](https://en.wikipedia.org/wiki/Domain_name):port
* Example http://gameserver.com:8080
`CustomLocale`
* Specifies a [locale name](https://msdn.microsoft.com/en-us/library/39cwe7zf.aspx) to use instead of system default
* Locale must be from the `Equivalent Locale Name` column
`ConnectionString`
* Specifies the [connection string](https://www.connectionstrings.com/mysql/) to a MySQL server that is used instead of SQLite
`RConPollRate`
* Specifies (in milliseconds) how often to poll each server for updates
`VpnExceptionIds`
* Specifies the list of `Client IDs` exempt from the VPN check (if enabled)
`Servers`
* Specifies the list of servers **IW4MAdmin** will monitor
@ -67,12 +97,22 @@ If you wish to further customize your experience of **IW4MAdmin**, the following
* Specifies the list of messages that are broadcasted to the particular server
* `Rules`
* Specifies the list of rules that apply to the particular server
* `ReservedSlotNumber`
* Specifies the number of client slots to reserve for privileged users
`AutoMessagePeriod`
* Specifies (in seconds) how often messages should be broadcasted to the server(s)
* Specifies (in seconds) how often messages should be broadcasted to each server
`AutoMessages`
* Specifies the list of messages that are broadcasted to **all** servers
* Specially formatted **tokens** can be used to broadcast dynamic information
* `{{TOTALPLAYERS}}` &mdash; displays how many players have connected
* `{{TOPSTATS}}` &mdash; displays the top 5 players on the server based on performance
* `{{MOSTPLAYED}}` &mdash; displays the top 5 players based on number of kills
* `{{TOTALPLAYTIME}}` &mdash; displays the cumulative play time (in man-hours) on all monitored servers
* `{{VERSION}}` &mdash; displays the version of **IW4MAdmin**
* `{{ADMINS}}` &mdash; displays the currently connected and *unmasked* privileged users online
* `{{NEXTMAP}} &dmash; displays the next map in rotation
`GlobalRules`
* Specifies the list of rules that apply to **all** servers`
@ -88,44 +128,48 @@ ___
### Commands
|Name |Alias|Description |Requires Target|Syntax |Required Level|
|--------------| -----| --------------------------------------------------------| -----------------| -------------| ----------------|
|prune|pa|demote any admins that have not connected recently (defaults to 30 days)|False|!pa \<optional inactive days\>|Owner|
|prune|pa|demote any privileged clients that have not connected recently (defaults to 30 days)|False|!pa \<optional inactive days\>|Owner|
|quit|q|quit IW4MAdmin|False|!q |Owner|
|rcon|rcon|send rcon command to server|False|!rcon \<command\>|Owner|
|ban|b|permanently ban a player from the server|True|!b \<player\> \<reason\>|SeniorAdmin|
|unban|ub|unban player by database id|True|!ub \<databaseID\> \<reason\>|SeniorAdmin|
|find|f|find player in database|False|!f \<player\>|Administrator|
|rcon|rcon|send rcon command to server|False|!rcon \<commands\>|Owner|
|ban|b|permanently ban a client from the server|True|!b \<player\> \<reason\>|SeniorAdmin|
|unban|ub|unban client by client id|True|!ub \<client id\> \<reason\>|SeniorAdmin|
|find|f|find client in database|False|!f \<player\>|Administrator|
|killserver|kill|kill the game server|False|!kill |Administrator|
|map|m|change to specified map|False|!m \<map\>|Administrator|
|maprotate|mr|cycle to the next map in rotation|False|!mr |Administrator|
|plugins|p|view all loaded plugins|False|!p |Administrator|
|alias|known|get past aliases and ips of a player|True|!known \<player\>|Moderator|
|baninfo|bi|get information about a ban for a player|True|!bi \<player\>|Moderator|
|tempban|tb|temporarily ban a client for specified time (defaults to 1 hour)|True|!tb \<player\> \<duration (m\|h\|d\|w\|y)\> \<reason\>|Administrator|
|alias|known|get past aliases and ips of a client|True|!known \<player\>|Moderator|
|baninfo|bi|get information about a ban for a client|True|!bi \<player\>|Moderator|
|fastrestart|fr|fast restart current map|False|!fr |Moderator|
|flag|fp|flag a suspicious player and announce to admins on join|True|!fp \<player\> \<reason\>|Moderator|
|flag|fp|flag a suspicious client and announce to admins on join|True|!fp \<player\> \<reason\>|Moderator|
|kick|k|kick a client by name|True|!k \<player\> \<reason\>|Moderator|
|list|l|list active clients|False|!l |Moderator|
|mask|hide|hide your presence as an administrator|False|!hide |Moderator|
|mask|hide|hide your presence as a privileged client|False|!hide |Moderator|
|reports|reps|get or clear recent reports|False|!reps \<optional clear\>|Moderator|
|say|s|broadcast message to all players|False|!s \<message\>|Moderator|
|setlevel|sl|set player to specified administration level|True|!sl \<player\> \<level\>|Moderator|
|say|s|broadcast message to all clients|False|!s \<message\>|Moderator|
|setlevel|sl|set client to specified privilege level|True|!sl \<player\> \<level\>|Moderator|
|setpassword|sp|set your authentication password|False|!sp \<password\>|Moderator|
|tempban|tb|temporarily ban a player for specified time (defaults to 1 hour)|True|!tb \<player\> \<duration (m\|h\|d\|w\|y)\> \<reason\>|Moderator|
|unflag|uf|Remove flag for client|True|!uf \<player\>|Moderator|
|uptime|up|get current application running time|False|!up |Moderator|
|usage|us|get current application memory usage|False|!us |Moderator|
|kick|k|kick a player by name|True|!k \<player\> \<reason\>|Trusted|
|login|l|login using password|False|!l \<password\>|Trusted|
|warn|w|warn player for infringing rules|True|!w \<player\> \<reason\>|Trusted|
|warnclear|wc|remove all warning for a player|True|!wc \<player\>|Trusted|
|admins|a|list currently connected admins|False|!a |User|
|usage|us|get application memory usage|False|!us |Moderator|
|balance|bal|balance teams|False|!bal |Trusted|
|login|li|login using password|False|!li \<password\>|Trusted|
|warn|w|warn client for infringing rules|True|!w \<player\> \<reason\>|Trusted|
|warnclear|wc|remove all warnings for a client|True|!wc \<player\>|Trusted|
|admins|a|list currently connected privileged clients|False|!a |User|
|getexternalip|ip|view your external IP address|False|!ip |User|
|help|h|list all available commands|False|!h \<optional command\>|User|
|ping|pi|get client's ping|False|!pi \<optional client\>|User|
|privatemessage|pm|send message to other player|True|!pm \<player\> \<message\>|User|
|report|rep|report a player for suspicious behavior|True|!rep \<player\> \<reason\>|User|
|help|h|list all available commands|False|!h \<optional commands\>|User|
|mostplayed|mp|view the top 5 dedicated players on the server|False|!mp |User|
|owner|iamgod|claim ownership of the server|False|!iamgod |User|
|ping|pi|get client's latency|False|!pi \<optional player\>|User|
|privatemessage|pm|send message to other client|True|!pm \<player\> \<message\>|User|
|report|rep|report a client for suspicious behavior|True|!rep \<player\> \<reason\>|User|
|resetstats|rs|reset your stats to factory-new|False|!rs |User|
|rules|r|list server rules|False|!r |User|
|stats|xlrstats|view your stats|False|!xlrstats \<optional player\>|User|
|topstats|ts|view the top 5 players on this server|False|!ts |User|
|whoami|who|give information about yourself.|False|!who |User|
|topstats|ts|view the top 5 players in this server|False|!ts |User|
|whoami|who|give information about yourself|False|!who |User|
_These commands include all shipped plugin commands._
@ -192,6 +236,7 @@ ___
|resetstats|rs|reset your stats to factory-new|False|!rs |User|
|stats|xlrstats|view your stats|False|!xlrstats \<optional player\>|User|
|topstats|ts|view the top 5 players on this server|False|!ts |User|
|mostplayed|mp|view the top 5 dedicated players on the server|False|!mp |User|
- To qualify for top stats, a client must have played for at least `1 hour` and connected within the past `30 days`.
@ -208,6 +253,7 @@ ___
#### Profanity Determent
- This plugin warns and kicks players for using profanity
- Profane words and warning message can be specified in `ProfanityDetermentSettings.json`
- If a client's name contains a word listed in the settings, they will immediately be kicked
___
### Webfront
`Home`
@ -221,6 +267,7 @@ ___
`Login`
* Allows privileged users to login using their `Client ID` and password set via `setpassword`
* `ClientID` is a number that can be found by using `!find <client name>` or find the client on the webfront and copy the ID following `ProfileAsync/`
`Profile`
* Shows a client's information and history
@ -229,7 +276,78 @@ ___
* Allows logged in privileged users to execute commands as if they are in-game
---
### Extending Plugins
#### Code
IW4Madmin functionality can be extended by writing additional plugins in C#.
#### JavaScript
IW4MAdmin functionality can be extended using JavaScript.
The JavaScript parser supports [some](https://github.com/sebastienros/jint/issues/343) of ECMAScript 6's new features.
#### Plugin Object Template
In order to be properly parsed by the JavaScript engine, every plugin must conform to the following template.
```js
const plugin = {
author: 'YourHandle',
version: 1.0,
name: 'Sample JavaScript Plugin',
### Misc
onEventAsync(gameEvent, server) {
},
onLoadAsync(manager) {
},
onUnloadAsync() {
},
onTickAsync(server) {
}
}
```
#### Required Properties
- `author` &mdash; [string] Author of the plugin (usually your name or online name/alias)
- `version` &mdash; [float] Version number of your plugin (useful if you release several different versions)
- `name` &mdash; [string] Name of your plugin (be descriptive!)
- `onEventAsync` &mdash; [function] Handler executed when an event occurs
- `gameEvent` &mdash; [parameter object] Object containing event type, origin, target, and other info (see the GameEvent class declaration)
- `server` &mdash; [parameter object] Object containing information and methods about the server the event occured on (see the Server class declaration)
- `onLoadAsync` &mdash; [function] Handler executed when the plugin is loaded by code
- `manager` &mdash; [parameter object] Object reference to the application manager (see the IManager interface definition)
- `onUnloadAsync` &mdash; [function] Handler executed when the plugin is unloaded by code (see live reloading)
- `onTickAsync` &mdash; [function] Handler executed approximately once per second by code *(unimplemented as of version 2.1)*
- `server` &mdash; [parameter object] Object containing information and methods about the server the event occured on (see the Server class declaration)
### Live Reloading
Thanks to JavaScript's flexibility and parsability, the plugin importer scans the plugins folder and reloads the JavaScript plugins on demand as they're modified. This allows faster development/testing/debugging.
---
### Discord Webhook
If you'd like to receive notifications on your Discord guild, configure and start `DiscordWebhook.py`
#### Requirements
- [Python 3.6](https://www.python.org/downloads/) or newer
- The following [PIP](https://pypi.org/project/pip/) packages (provided in `requirements.txt`)
```certifi>=2018.4.16
chardet>=3.0.4
idna>=2.7
pip>=18.0
requests>=2.19.1
setuptools>=39.0.1
urllib3>=1.23
```
#### Configuration Options
- `IW4MAdminUrl` &mdash; Base url corresponding to your IW4MAdmin `WebfrontBindUrl`.
Example http://127.0.0.1
- `DiscordWebhookNotificationUrl` &mdash; [required] Discord generated URL to send notifications/alerts to; this includes **Reports** and **Bans**
Example https://discordapp.com/api/webhooks/id/token
- `DiscordWebhookInformationUrl` &mdash; [optional] Discord generated URL to send information to; this includes information such as player messages
- `NotifyRoleIds` &mdash; [optional] List of [discord role ids](https://discordhelp.net/role-id) to mention when notification hook is sent
#### Launching
With Python installed, open a terminal/command prompt window open in the `Webhook` folder and execute `python DiscordWebhook.py`
---
## Misc
#### Anti-cheat
This is an [IW4x](https://iw4xcachep26muba.onion.link/) only feature (wider game support planned), that uses analytics to detect aimbots and aim-assist tools.
To utilize anti-cheat, enable it during setup **and** copy `_customcallbacks.gsc` from `userraw` into your `IW4x Server\userraw\scripts` folder.
The anti-cheat feature is a work in progress and as such will be constantly tweaked and may not be 100% accurate, however the goal is to deter as many cheaters as possible from IW4x.
#### Database Storage
All **IW4MAdmin** information is stored in `Database.db`. Should you need to reset your database, this file can simply be deleted. Additionally, this file should be preserved during updates to retain client information.
By default, all **IW4MAdmin** information is stored in `Database.db`. Should you need to reset your database, this file can simply be deleted. Additionally, this file should be preserved during updates to retain client information.
Setting the `ConnectionString` property in `IW4MAdminSettings.json` will cause **IW4MAdmin** to attempt to use a MySQL connection for database storage.

View File

@ -22,13 +22,13 @@ namespace SharedLibraryCore.Configuration
public string CustomLocale { get; set; }
public string ConnectionString { get; set; }
public int RConPollRate { get; set; } = 5000;
public List<int> VpnExceptionIds { get; set; }
public string Id { get; set; }
public List<ServerConfiguration> Servers { get; set; }
public int AutoMessagePeriod { get; set; }
public List<string> AutoMessages { get; set; }
public List<string> GlobalRules { get; set; }
public List<MapConfiguration> Maps { get; set; }
public List<int> VpnExceptionIds { get; set; }
public IBaseConfiguration Generate()
{

View File

@ -9,8 +9,8 @@ namespace SharedLibraryCore.Configuration
public string IPAddress { get; set; }
public ushort Port { get; set; }
public string Password { get; set; }
public List<string> Rules { get; set; }
public List<string> AutoMessages { get; set; }
public IList<string> Rules { get; set; }
public IList<string> AutoMessages { get; set; }
public bool UseT6MParser { get; set; }
public bool UseIW5MParser { get; set; }
public string ManualLogPath { get; set; }

View File

@ -87,6 +87,7 @@ namespace SharedLibraryCore
{
return queuedEvent.Origin != null &&
!queuedEvent.Origin.IsAuthenticated &&
queuedEvent.Origin.State != Player.ClientState.Connected &&
// we want to allow join and quit events
queuedEvent.Type != EventType.Join &&
queuedEvent.Type != EventType.Quit &&
@ -104,6 +105,7 @@ namespace SharedLibraryCore
{
return queuedEvent.Target != null &&
!queuedEvent.Target.IsAuthenticated &&
queuedEvent.Target.State != Player.ClientState.Connected &&
queuedEvent.Target.NetworkId != 0;
}
}

View File

@ -13,37 +13,33 @@ namespace SharedLibraryCore.Plugins
public static List<IPlugin> ActivePlugins = new List<IPlugin>();
public static List<Assembly> PluginAssemblies = new List<Assembly>();
private static void LoadScriptPlugins(IManager mgr)
{
string[] scriptFileNames = Directory.GetFiles($"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", "*.js");
foreach(string fileName in scriptFileNames)
{
var plugin = new ScriptPlugin(fileName);
plugin.Initialize(mgr).Wait();
ActivePlugins.Add(plugin);
}
}
public static bool Load(IManager Manager)
{
string[] dllFileNames = Directory.GetFiles($"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", "*.dll");
string[] scriptFileNames = Directory.GetFiles($"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", "*.js");
if (dllFileNames.Length == 0)
if (dllFileNames.Length == 0 &&
scriptFileNames.Length == 0)
{
Manager.GetLogger().WriteDebug(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_IMPORTER_NOTFOUND"]);
return true;
}
// load up the script plugins
foreach (string fileName in scriptFileNames)
{
var plugin = new ScriptPlugin(fileName);
plugin.Initialize(Manager).Wait();
Manager.GetLogger().WriteDebug($"Loaded script plugin \"{ plugin.Name }\" [{plugin.Version}]");
ActivePlugins.Add(plugin);
}
ICollection<Assembly> assemblies = new List<Assembly>(dllFileNames.Length);
foreach (string dllFile in dllFileNames)
{
// byte[] rawDLL = File.ReadAllBytes(dllFile);
//Assembly assembly = Assembly.Load(rawDLL);
assemblies.Add(Assembly.LoadFrom(dllFile));
}
int LoadedPlugins = 0;
int LoadedCommands = 0;
foreach (Assembly Plugin in assemblies)
{
@ -74,19 +70,17 @@ namespace SharedLibraryCore.Plugins
ActivePlugins.Add(newNotify);
PluginAssemblies.Add(Plugin);
Manager.GetLogger().WriteDebug($"Loaded plugin \"{ newNotify.Name }\" [{newNotify.Version}]");
LoadedPlugins++;
}
}
catch (Exception E)
{
Manager.GetLogger().WriteWarning($"Could not load plugin {Plugin.Location} - {E.Message}");
Manager.GetLogger().WriteWarning($"{Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_IMPORTER_ERROR"]} {Plugin.Location} - {E.Message}");
}
}
}
}
LoadScriptPlugins(Manager);
Manager.GetLogger().WriteInfo($"Loaded {LoadedPlugins} plugins and registered {LoadedCommands} commands.");
Manager.GetLogger().WriteInfo($"Loaded {ActivePlugins.Count} plugins and registered {LoadedCommands} commands.");
return true;
}
}

View File

@ -49,15 +49,22 @@ namespace SharedLibraryCore
public async Task Initialize(IManager mgr)
{
bool firstRun = ScriptEngine == null;
// it's been loaded before so we need to call the unload event
if (ScriptEngine != null)
if (!firstRun)
{
await OnUnloadAsync();
}
Manager = mgr;
string script = File.ReadAllText(FileName);
ScriptEngine = new Jint.Engine();
ScriptEngine = new Jint.Engine(cfg =>
cfg.AllowClr(new[]
{
typeof(System.Net.WebRequest).Assembly,
typeof(Objects.Player).Assembly,
})
.CatchClrExceptions());
ScriptEngine.Execute(script);
ScriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
@ -67,7 +74,7 @@ namespace SharedLibraryCore
this.Name = pluginObject.name;
this.Version = (float)pluginObject.version;
if (ScriptEngine != null)
if (!firstRun)
{
await OnLoadAsync(mgr);
}

View File

@ -20,6 +20,16 @@ namespace SharedLibraryCore
public static Encoding EncodingType;
public static Localization.Layout CurrentLocalization;
public static string HttpRequest(string location, string header, string headerValue)
{
using (var RequestClient = new System.Net.Http.HttpClient())
{
RequestClient.DefaultRequestHeaders.Add(header, headerValue);
string response = RequestClient.GetStringAsync(location).Result;
return response;
}
}
//Get string with specified number of spaces -- really only for visual output
public static String GetSpaces(int Num)
{

View File

@ -7,7 +7,7 @@
}
<div id="profile_wrapper" class="row d-flex d-sm-inline-flex justify-content-center justify-content-left pb-3">
<div class="mr-auto ml-auto ml-sm-0 mr-sm-0">
<div id="profile_avatar" class="mb-4 mb-md-0 text-center level-bgcolor-@Model.LevelInt" style="background-image:url('@string.Format("https://gravatar.com/avatar/{0}?size=168&default=blank&rating=pg", gravatarUrl)">
<div id="profile_avatar" class="mb-4 mb-md-0 text-center level-bgcolor-@Model.LevelInt" style="background-image:url('@string.Format("https://gravatar.com/avatar/{0}?size=168&default=blank&rating=pg", gravatarUrl)')">
@if (string.IsNullOrEmpty(gravatarUrl))
{
<span class="profile-shortcode">@shortCode</span>

View File

@ -29,7 +29,7 @@
<tr class="d-table-row d-md-none bg-dark">
<th scope="row" class="bg-primary">@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</th>
<td>
@Html.ActionLink(Model.PunisherName, "ProfileAsync", "Client", new { id = Model.PunisherId }, new { @class = "level-color-" + Model.PunisherLevelId }) })
@Html.ActionLink(Model.PunisherName, "ProfileAsync", "Client", new { id = Model.PunisherId }, new { @class = "level-color-" + Model.PunisherLevelId })
</td>
</tr>
@ -43,7 +43,7 @@
}
else
{
<span> @Model.TimeRemaining @loc["WEBFRONT_PENALTY_TEMPLATE_REMAINING"]</span>
<span> @Model.TimeRemaining</span>
}
}
</td>
@ -60,7 +60,7 @@
@Model.Offense
</td>
<td>
@Html.ActionLink(Model.PunisherName, "ProfileAsync", "Client", new { id = Model.PunisherId }, new { @class = "level-color-" + Model.PunisherLevelId }) })
@Html.ActionLink(Model.PunisherName, "ProfileAsync", "Client", new { id = Model.PunisherId }, new { @class = "level-color-" + Model.PunisherLevelId })
</td>
<td class="text-right text-light">
@{
@ -70,7 +70,7 @@
}
else
{
<span> @Model.TimeRemaining <!-- @loc["WEBFRONT_PENALTY_TEMPLATE_REMAINING"] --></span>
<span> @Model.TimeRemaining </span>
}
}
</td>

View File

@ -57,7 +57,7 @@
color: rgba(236, 130, 222, 0.69);
}
.level-bgcolor-administrator, .level.bgcolor-4 {
.level-bgcolor-administrator, .level-bgcolor-4 {
background-color: #f1a8e8;
background-color: rgba(236, 130, 222, 0.69);
}
@ -80,6 +80,14 @@
background-color: rgb(0, 122, 204);
}
.level-color-8 {
color: #de4423;
}
.level-bgcolor-8 {
background-color: #de4423;
}
.profile-meta-title {
color: white;
}

View File

@ -1,5 +1,21 @@
Version 2.2:
-upgraded projects to .NET 2.1
-added top player stats page
-added JavaScript plugin support
-added webhook script to send notifications to discord
-added abillity to exempt specific clients from VPN check
-added reserved slots for privileged users
-added support for localized permission levels
-added linux support!
-added {{NEXTMAP}}, {{ADMINS}}, and {{MOSTPLAYED}} automessage tokens
-updated event api
-updated webfront tweak
-update client search by IP
-updated event management and client authentication
-fixed some namespace discrepancies
-fixed parsing of certain chat messages
-fixed various bugs
-introduced new bugs to fix in the next version
Version 2.1:
CHANGELOG: