RCon error handling is clearer

Show chat on mobile view of server overview
basic authentication
switched to extreme-ip-lookup for ip lookups (SSL)
This commit is contained in:
RaidMax 2018-04-04 14:38:34 -05:00
parent a0c1d9b1bc
commit c0865b82a0
27 changed files with 476 additions and 123 deletions

View File

@ -975,6 +975,29 @@ namespace SharedLibrary.Commands
} }
} }
public class CSetPassword : Command
{
public CSetPassword() : base("setpassword", "set your authentication password", "sp", Player.Permission.Moderator, false, new CommandArgument[]
{
new CommandArgument()
{
Name = "password",
Required = true
}
})
{ }
public override async Task ExecuteAsync(Event E)
{
string[] hashedPassword = Helpers.Hashing.Hash(E.Data);
E.Origin.Password = hashedPassword[0];
E.Origin.PasswordSalt = hashedPassword[1];
await E.Owner.Manager.GetClientService().Update(E.Origin);
}
}
public class CKillServer : Command public class CKillServer : Command
{ {
public CKillServer() : base("killserver", "kill the game server", "kill", Player.Permission.Administrator, false) public CKillServer() : base("killserver", "kill the game server", "kill", Player.Permission.Administrator, false)

View File

@ -36,6 +36,9 @@ namespace SharedLibrary.Database.Models
[ForeignKey("CurrentAliasId")] [ForeignKey("CurrentAliasId")]
public virtual EFAlias CurrentAlias { get; set; } public virtual EFAlias CurrentAlias { get; set; }
public string Password { get; set; }
public string PasswordSalt { get; set; }
[NotMapped] [NotMapped]
public virtual string Name public virtual string Name
{ {

View File

@ -0,0 +1,77 @@
using System;
using SimpleCrypto;
namespace SharedLibrary.Helpers
{
public class Hashing
{
/// <summary>
/// Generate password hash and salt
/// </summary>
/// <param name="password">plaintext password</param>
/// <returns></returns>
public static string[] Hash(string password, string saltStr = null)
{
string hash;
string salt;
var CryptoSvc = new PBKDF2();
// generate new hash
if (saltStr == null)
{
hash = CryptoSvc.Compute(password);
salt = CryptoSvc.Salt;
return new string[]
{
hash,
salt
};
}
else
{
hash = CryptoSvc.Compute(password, saltStr);
return new string[]
{
hash,
""
};
}
/*//https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/password-hashing
byte[] salt;
if (saltStr == null)
{
salt = new byte[128 / 8];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}
}
else
{
salt = Convert.FromBase64String(saltStr);
}
// derive a 256-bit subkey (use HMACSHA1 with 10,000 iterations)
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: password,
salt: salt,
prf: KeyDerivationPrf.HMACSHA1,
iterationCount: 10000,
numBytesRequested: 256 / 8));
return new string[]
{
hashed,
Convert.ToBase64String(salt)
};*/
}
}
}

View File

@ -11,10 +11,24 @@ namespace SharedLibrary.RCon
{ {
class ConnectionState class ConnectionState
{ {
public Socket Client { get; set; } public Socket Client { get; private set; }
public const int BufferSize = 8192; public int BufferSize { get; private set; }
public byte[] Buffer = new byte[BufferSize]; public byte[] Buffer { get; private set; }
public readonly StringBuilder ResponseString = new StringBuilder();
private readonly StringBuilder sb;
public StringBuilder ResponseString
{
get => sb;
}
public ConnectionState(Socket cl)
{
BufferSize = 8192;
Buffer = new byte[BufferSize];
Client = cl;
sb = new StringBuilder();
}
} }
public class Connection public class Connection
@ -26,6 +40,7 @@ namespace SharedLibrary.RCon
int FailedSends; int FailedSends;
int FailedReceives; int FailedReceives;
DateTime LastQuery; DateTime LastQuery;
string response;
ManualResetEvent OnConnected; ManualResetEvent OnConnected;
ManualResetEvent OnSent; ManualResetEvent OnSent;
@ -92,14 +107,11 @@ namespace SharedLibrary.RCon
#if DEBUG #if DEBUG
Log.WriteDebug($"Sent {sentByteNum} bytes to {ServerConnection.RemoteEndPoint}"); Log.WriteDebug($"Sent {sentByteNum} bytes to {ServerConnection.RemoteEndPoint}");
#endif #endif
FailedSends = 0;
OnSent.Set(); OnSent.Set();
} }
catch (Exception e) catch (SocketException)
{ {
FailedSends++;
Log.WriteWarning($"Could not send RCon data to server - {e.Message}");
} }
} }
@ -126,31 +138,33 @@ namespace SharedLibrary.RCon
} }
else else
{ {
FailedReceives = 0; response = connectionState.ResponseString.ToString();
OnReceived.Set(); OnReceived.Set();
} }
} }
else else
{ {
response = connectionState.ResponseString.ToString();
OnReceived.Set(); OnReceived.Set();
} }
} }
catch (Exception) catch (SocketException)
{ {
FailedReceives++;
} }
} }
public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "") public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "")
{ {
// will this really prevent flooding? // will this really prevent flooding?
if ((DateTime.Now - LastQuery).TotalMilliseconds < 300) if ((DateTime.Now - LastQuery).TotalMilliseconds < 150)
{ {
await Task.Delay(300); await Task.Delay(150);
LastQuery = DateTime.Now;
} }
LastQuery = DateTime.Now;
OnSent.Reset(); OnSent.Reset();
OnReceived.Reset(); OnReceived.Reset();
string queryString = ""; string queryString = "";
@ -168,66 +182,103 @@ namespace SharedLibrary.RCon
byte[] payload = Encoding.Default.GetBytes(queryString); byte[] payload = Encoding.Default.GetBytes(queryString);
retrySend:
try try
{ {
retrySend:
ServerConnection.BeginSend(payload, 0, payload.Length, 0, new AsyncCallback(OnSentCallback), ServerConnection); ServerConnection.BeginSend(payload, 0, payload.Length, 0, new AsyncCallback(OnSentCallback), ServerConnection);
bool success = await Task.FromResult(OnSent.WaitOne(StaticHelpers.SocketTimeout)); bool success = await Task.FromResult(OnSent.WaitOne(StaticHelpers.SocketTimeout));
if (!success) if (!success)
{ {
FailedSends++;
#if DEBUG #if DEBUG
Log.WriteDebug($"{FailedSends} failed sends to {ServerConnection.RemoteEndPoint.ToString()}"); Log.WriteDebug($"{FailedSends} failed sends to {ServerConnection.RemoteEndPoint.ToString()}");
#endif #endif
if (FailedSends < 4) if (FailedSends < 4)
goto retrySend; goto retrySend;
else if (FailedSends == 4)
Log.WriteError($"Failed to send data to {ServerConnection.RemoteEndPoint}");
}
else else
throw new NetworkException($"Could not send data to server - {new SocketException((int)SocketError.TimedOut).Message}"); {
if (FailedSends >= 4)
{
Log.WriteVerbose($"Resumed send RCon connection with {ServerConnection.RemoteEndPoint}");
FailedSends = 0;
}
} }
} }
catch (SocketException e) catch (SocketException e)
{ {
// this result is normal if the server is not listening // this result is normal if the server is not listening
if (e.HResult != (int)SocketError.ConnectionReset) if (e.NativeErrorCode != (int)SocketError.ConnectionReset &&
e.NativeErrorCode != (int)SocketError.TimedOut)
throw new NetworkException($"Unexpected error while sending data to server - {e.Message}"); throw new NetworkException($"Unexpected error while sending data to server - {e.Message}");
} }
var connectionState = new ConnectionState var connectionState = new ConnectionState(ServerConnection);
{
Client = ServerConnection
};
retryReceive:
try try
{ {
retryReceive:
ServerConnection.BeginReceive(connectionState.Buffer, 0, connectionState.Buffer.Length, 0, ServerConnection.BeginReceive(connectionState.Buffer, 0, connectionState.Buffer.Length, 0,
new AsyncCallback(OnReceivedCallback), connectionState); new AsyncCallback(OnReceivedCallback), connectionState);
bool success = await Task.FromResult(OnReceived.WaitOne(StaticHelpers.SocketTimeout)); bool success = await Task.FromResult(OnReceived.WaitOne(StaticHelpers.SocketTimeout));
if (!success) if (!success)
{ {
FailedReceives++;
#if DEBUG #if DEBUG
Log.WriteDebug($"{FailedReceives} failed receives from {ServerConnection.RemoteEndPoint.ToString()}"); Log.WriteDebug($"{FailedReceives} failed receives from {ServerConnection.RemoteEndPoint.ToString()}");
#endif #endif
FailedReceives++;
if (FailedReceives < 4) if (FailedReceives < 4)
goto retryReceive; goto retrySend;
else if (FailedReceives == 4) else if (FailedReceives == 4)
Log.WriteError($"Failed to receive data from {ServerConnection.RemoteEndPoint}"); {
Log.WriteError($"Failed to receive data from {ServerConnection.RemoteEndPoint} after {FailedReceives} tries");
}
if (FailedReceives >= 4)
{
throw new NetworkException($"Could not receive data from the {ServerConnection.RemoteEndPoint}");
}
}
else else
throw new NetworkException($"Could not receive data from the server - {new SocketException((int)SocketError.TimedOut).Message}"); {
if (FailedReceives >= 4)
{
Log.WriteVerbose($"Resumed receive RCon connection from {ServerConnection.RemoteEndPoint}");
FailedReceives = 0;
}
} }
} }
catch (SocketException e) catch (SocketException e)
{ {
// this result is normal if the server is not listening // this result is normal if the server is not listening
if (e.HResult != (int)SocketError.ConnectionReset) if (e.NativeErrorCode != (int)SocketError.ConnectionReset &&
e.NativeErrorCode != (int)SocketError.TimedOut)
throw new NetworkException($"Unexpected error while receiving data from server - {e.Message}"); throw new NetworkException($"Unexpected error while receiving data from server - {e.Message}");
else if (FailedReceives < 4)
{
goto retryReceive;
} }
string queryResponse = connectionState.ResponseString.ToString(); else if (FailedReceives == 4)
{
Log.WriteError($"Failed to receive data from {ServerConnection.RemoteEndPoint} after {FailedReceives} tries");
}
if (FailedReceives >= 4)
{
throw new NetworkException(e.Message);
}
}
string queryResponse = response;
if (queryResponse.Contains("Invalid password")) if (queryResponse.Contains("Invalid password"))
throw new NetworkException("RCON password is invalid"); throw new NetworkException("RCON password is invalid");

View File

@ -65,7 +65,7 @@ namespace SharedLibrary.Services
Masked = false, Masked = false,
NetworkId = entity.NetworkId, NetworkId = entity.NetworkId,
AliasLink = aliasLink, AliasLink = aliasLink,
CurrentAlias = existingAlias CurrentAlias = existingAlias,
}; };
context.Clients.Add(client); context.Clients.Add(client);
@ -180,6 +180,8 @@ namespace SharedLibrary.Services
client.FirstConnection = entity.FirstConnection; client.FirstConnection = entity.FirstConnection;
client.Masked = entity.Masked; client.Masked = entity.Masked;
client.TotalConnectionTime = entity.TotalConnectionTime; client.TotalConnectionTime = entity.TotalConnectionTime;
client.Password = entity.Password;
client.PasswordSalt = entity.PasswordSalt;
// update in database // update in database
await context.SaveChangesAsync(); await context.SaveChangesAsync();

View File

@ -173,6 +173,7 @@
<Compile Include="Exceptions\SerializationException.cs" /> <Compile Include="Exceptions\SerializationException.cs" />
<Compile Include="Exceptions\ServerException.cs" /> <Compile Include="Exceptions\ServerException.cs" />
<Compile Include="Helpers\BaseConfigurationHandler.cs" /> <Compile Include="Helpers\BaseConfigurationHandler.cs" />
<Compile Include="Helpers\Hashing.cs" />
<Compile Include="Helpers\ParseEnum.cs" /> <Compile Include="Helpers\ParseEnum.cs" />
<Compile Include="Helpers\Vector3.cs" /> <Compile Include="Helpers\Vector3.cs" />
<Compile Include="Interfaces\IBaseConfiguration.cs" /> <Compile Include="Interfaces\IBaseConfiguration.cs" />
@ -242,6 +243,9 @@
<PackageReference Include="Newtonsoft.Json"> <PackageReference Include="Newtonsoft.Json">
<Version>11.0.1</Version> <Version>11.0.1</Version>
</PackageReference> </PackageReference>
<PackageReference Include="SimpleCrypto">
<Version>0.3.30.26</Version>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.5.2"> <BootstrapperPackage Include=".NETFramework,Version=v4.5.2">

View File

@ -78,16 +78,26 @@ namespace IW4MAdmin
{ {
#region DATABASE #region DATABASE
var ipList = (await ClientSvc.Find(c => c.Level > Player.Permission.Trusted)) var ipList = (await ClientSvc.Find(c => c.Level > Player.Permission.Trusted))
.Select(c => new { c.IPAddress, c.ClientId, c.Level }); .Select(c => new
{
c.Password,
c.PasswordSalt,
c.ClientId,
c.Level,
c.Name
});
foreach (var a in ipList) foreach (var a in ipList)
{ {
try try
{ {
PrivilegedClients.Add(a.IPAddress, new Player() PrivilegedClients.Add(a.ClientId, new Player()
{ {
Name = a.Name,
ClientId = a.ClientId, ClientId = a.ClientId,
Level = a.Level Level = a.Level,
PasswordSalt = a.PasswordSalt,
Password = a.Password
}); });
} }
@ -213,6 +223,7 @@ namespace IW4MAdmin
Commands.Add(new CMask()); Commands.Add(new CMask());
Commands.Add(new CPruneAdmins()); Commands.Add(new CPruneAdmins());
Commands.Add(new CKillServer()); Commands.Add(new CKillServer());
Commands.Add(new CSetPassword());
foreach (Command C in SharedLibrary.Plugins.PluginImporter.ActiveCommands) foreach (Command C in SharedLibrary.Plugins.PluginImporter.ActiveCommands)
Commands.Add(C); Commands.Add(C);

View File

@ -791,16 +791,26 @@ namespace IW4MAdmin
((ApplicationManager)(Manager)).PrivilegedClients = new Dictionary<int, Player>(); ((ApplicationManager)(Manager)).PrivilegedClients = new Dictionary<int, Player>();
var ClientSvc = new ClientService(); var ClientSvc = new ClientService();
var ipList = (await ClientSvc.Find(c => c.Level > Player.Permission.Trusted)) var ipList = (await ClientSvc.Find(c => c.Level > Player.Permission.Trusted))
.Select(c => new { c.IPAddress, c.ClientId, c.Level }); .Select(c => new
{
c.Password,
c.PasswordSalt,
c.ClientId,
c.Level,
c.Name
});
foreach (var a in ipList) foreach (var a in ipList)
{ {
try try
{ {
((ApplicationManager)(Manager)).PrivilegedClients.Add(a.IPAddress, new Player() ((ApplicationManager)(Manager)).PrivilegedClients.Add(a.ClientId, new Player()
{ {
Name = a.Name,
ClientId = a.ClientId, ClientId = a.ClientId,
Level = a.Level Level = a.Level,
PasswordSalt = a.PasswordSalt,
Password = a.Password
}); });
} }

View File

@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.Security.Claims;
namespace WebfrontCore.Controllers
{
public class AccountController : BaseController
{
[HttpGet]
public async Task<IActionResult> Login(int userId, string password)
{
if (userId == 0 || string.IsNullOrEmpty(password))
{
return Unauthorized();
}
var client = IW4MAdmin.Program.ServerManager.PrivilegedClients[userId];
string[] hashedPassword = await Task.FromResult(SharedLibrary.Helpers.Hashing.Hash(password, client.PasswordSalt));
if (hashedPassword[0] == client.Password)
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, client.Name),
new Claim(ClaimTypes.Role, client.Level.ToString()),
new Claim(ClaimTypes.Sid, client.ClientId.ToString())
};
var claimsIdentity = new ClaimsIdentity(claims, "login");
var claimsPrinciple = new ClaimsPrincipal(claimsIdentity);
await HttpContext.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrinciple);
return Ok();
}
return Unauthorized();
}
}
}

View File

@ -71,5 +71,34 @@ namespace WebfrontCore.Controllers
command = $"!unban @{targetId} {Reason}" command = $"!unban @{targetId} {Reason}"
})); }));
} }
public IActionResult LoginForm()
{
var login = new ActionInfo()
{
ActionButtonLabel = "Login",
Name = "Login",
Inputs = new List<InputInfo>()
{
new InputInfo()
{
Name = "UserID"
},
new InputInfo()
{
Name = "Password",
Type = "password",
}
},
Action = "Login"
};
return View("_ActionForm", login);
}
public IActionResult Login(int userId, string password)
{
return RedirectToAction("Login", "Account", new { userId, password });
}
} }
} }

View File

@ -3,7 +3,11 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using SharedLibrary; using SharedLibrary;
using SharedLibrary.Database.Models; using SharedLibrary.Database.Models;
using SharedLibrary.Objects;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
namespace WebfrontCore.Controllers namespace WebfrontCore.Controllers
{ {
@ -24,18 +28,17 @@ namespace WebfrontCore.Controllers
try try
{ {
var client = Manager.PrivilegedClients[context.HttpContext.Connection.RemoteIpAddress.ToString().ConvertToIP()]; User.ClientId = Convert.ToInt32(base.User.Claims.First(c => c.Type == ClaimTypes.Sid).Value);
User.ClientId = client.ClientId; User.Level = (Player.Permission)Enum.Parse(typeof(Player.Permission), base.User.Claims.First(c => c.Type == ClaimTypes.Role).Value);
User.Level = client.Level; User.CurrentAlias = new EFAlias() { Name = base.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value };
} }
catch (KeyNotFoundException) catch (InvalidOperationException)
{ {
} }
Authorized = context.HttpContext.Connection.RemoteIpAddress.ToString() == "127.0.0.1" || Authorized = User.ClientId >= 0;
User.ClientId >= 0;
ViewBag.Authorized = Authorized; ViewBag.Authorized = Authorized;
ViewBag.Url = Startup.Configuration["Web:Address"]; ViewBag.Url = Startup.Configuration["Web:Address"];
ViewBag.User = User; ViewBag.User = User;

View File

@ -63,7 +63,7 @@ namespace WebfrontCore.Controllers
.Select(a => new ProfileMeta() .Select(a => new ProfileMeta()
{ {
Key = "AliasEvent", Key = "AliasEvent",
Value = $"Connected with name {a.Name}", Value = $"Joined with alias {a.Name}",
Sensitive = true, Sensitive = true,
When = a.DateAdded When = a.DateAdded
})); }));

View File

@ -28,29 +28,16 @@ namespace WebfrontCore.Controllers
public async Task<IActionResult> ExecuteAsync(int serverId, string command) public async Task<IActionResult> ExecuteAsync(int serverId, string command)
{ {
var requestIPAddress = Request.HttpContext.Connection.RemoteIpAddress;
var intIP = requestIPAddress.ToString().ConvertToIP();
#if !DEBUG
var origin = (await IW4MAdmin.ApplicationManager.GetInstance().GetClientService().GetClientByIP(intIP))
.OrderByDescending(c => c.Level)
.FirstOrDefault()?.AsPlayer() ?? new Player()
{
Name = "WebConsoleUser",
Level = Player.Permission.User,
IPAddress = intIP
};
#else
var origin = (await Manager.GetClientService().GetUnique(0)).AsPlayer();
#endif
var server = Manager.Servers.First(s => s.GetHashCode() == serverId); var server = Manager.Servers.First(s => s.GetHashCode() == serverId);
origin.CurrentServer = server; var client = User.AsPlayer();
var remoteEvent = new Event(Event.GType.Say, command, origin, null, server); client.CurrentServer = server;
var remoteEvent = new Event(Event.GType.Say, command, client, null, server);
await server.ExecuteEvent(remoteEvent); await server.ExecuteEvent(remoteEvent);
var response = server.CommandResult.Where(c => c.ClientId == origin.ClientId).ToList(); var response = server.CommandResult.Where(c => c.ClientId == client.ClientId).ToList();
// remove the added command response // remove the added command response
for (int i = 0; i < response.Count; i++) for (int i = 0; i < response.Count; i++)

View File

@ -21,7 +21,7 @@ namespace WebfrontCore.Controllers
public IActionResult Error() public IActionResult Error()
{ {
ViewBag.Description = "IW4MAdmin encountered and error"; ViewBag.Description = "IW4MAdmin encountered an error";
ViewBag.Title = "Error!"; ViewBag.Title = "Error!";
return View(); return View();
} }

View File

@ -1,13 +1,10 @@
using System; using System;
using System.Collections.Generic; using Microsoft.AspNetCore.Authentication.Cookies;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibrary.Configuration;
namespace WebfrontCore namespace WebfrontCore
{ {
@ -52,12 +49,23 @@ namespace WebfrontCore
app.UseStaticFiles(); app.UseStaticFiles();
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AccessDeniedPath = "/Account/Login/",
AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme,
AutomaticAuthenticate = true,
AutomaticChallenge = true,
LoginPath = "/Account/Login/"
});
app.UseMvc(routes => app.UseMvc(routes =>
{ {
routes.MapRoute( routes.MapRoute(
name: "default", name: "default",
template: "{controller=Home}/{action=Index}/{id?}"); template: "{controller=Home}/{action=Index}/{id?}");
}); });
//app.UseBasicAuthentication(Authentication.Basic.Generate());
} }
} }
} }

View File

@ -19,8 +19,8 @@ namespace WebfrontCore.ViewComponents
try try
{ {
var a = IW4MAdmin.ApplicationManager.GetInstance() // var a = IW4MAdmin.ApplicationManager.GetInstance()
.PrivilegedClients[HttpContext.Connection.RemoteIpAddress.ToString().ConvertToIP()]; //.PrivilegedClients[HttpContext.Connection.RemoteIpAddress.ToString().ConvertToIP()];
} }
catch (KeyNotFoundException) catch (KeyNotFoundException)

View File

@ -3,20 +3,21 @@
Layout = null; Layout = null;
} }
<form class="action-form" action="/Action/@Model.Action"> <form class="action-form" action="/Action/@Model.Action">
<div class="input-group mb-3">
@foreach (var input in Model.Inputs) @foreach (var input in Model.Inputs)
{ {
<div class="input-group mb-3">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text" id="basic-addon-@input.Name">@input.Name</span> <span class="input-group-text" id="basic-addon-@input.Name">@input.Name</span>
</div> </div>
{ @{
string inputType = input.Type ?? "text"; string inputType = input.Type ?? "text";
string value = input.Value ?? ""; string value = input.Value ?? "";
<input type="@inputType" name="@input.Name" value="@value" class="form-control" placeholder="@input.Placeholder" aria-label="@input.Name" aria-describedby="basic-addon-@input.Name"> <input type="@inputType" name="@input.Name" value="@value" class="form-control" placeholder="@input.Placeholder" aria-label="@input.Name" aria-describedby="basic-addon-@input.Name">
} }
}
</div> </div>
}
<button type="submit" class="btn btn-block btn-primary">@Model.ActionButtonLabel</button> <button type="submit" class="btn btn-block btn-primary">@Model.ActionButtonLabel</button>
</form> </form>

View File

@ -23,13 +23,13 @@
@if (Model.LevelInt < (int)ViewBag.User.Level && @if (Model.LevelInt < (int)ViewBag.User.Level &&
(SharedLibrary.Objects.Player.Permission)Model.LevelInt != SharedLibrary.Objects.Player.Permission.Banned) (SharedLibrary.Objects.Player.Permission)Model.LevelInt != SharedLibrary.Objects.Player.Permission.Banned)
{ {
<div id="profile_action_ban_btn" class="profile-action oi oi-ban text-danger h3 ml-2" title="Ban Client" data-action="ban" aria-hidden="true"></div> <div id="profile_action_ban_btn" class="profile-action oi oi-lock-unlocked text-success h3 ml-2" title="Ban Client" data-action="ban" aria-hidden="true"></div>
} }
@if (Model.LevelInt < (int)ViewBag.User.Level && @if (Model.LevelInt < (int)ViewBag.User.Level &&
(SharedLibrary.Objects.Player.Permission)Model.LevelInt == SharedLibrary.Objects.Player.Permission.Banned) (SharedLibrary.Objects.Player.Permission)Model.LevelInt == SharedLibrary.Objects.Player.Permission.Banned)
{ {
<div id="profile_action_unban_btn" class="profile-action oi oi-action-undo text-success h3 ml-2" title="Unban Client" data-action="unban" aria-hidden="true"></div> <div id="profile_action_unban_btn" class="profile-action oi oi-lock-locked text-danger h3 ml-2" title="Unban Client" data-action="unban" aria-hidden="true"></div>
} }
</div> </div>

View File

@ -47,3 +47,28 @@
</div> </div>
</div> </div>
</div> </div>
@if (Model.ChatHistory.Length > 0)
{
<div class="w-100 border-bottom d-md-none d-block mt-1 mb-1"></div>
}
<div class="col-12 col-md-8 d-md-none d-block text-left">
@{
for (int i = 0; i < Model.ChatHistory.Length; i++)
{
string message = @Model.ChatHistory[i].Message;
if (Model.ChatHistory[i].Message == "CONNECTED")
{
<span class="text-light"><span class="oi oi-account-login mr-2 text-success"> </span>@Model.ChatHistory[i].Name</span><br />
}
if (Model.ChatHistory[i].Message == "DISCONNECTED")
{
<span class="text-light"><span class="oi oi-account-logout mr-2 text-danger"> </span>@Model.ChatHistory[i].Name</span><br />
}
if (Model.ChatHistory[i].Message != "CONNECTED" && Model.ChatHistory[i].Message != "DISCONNECTED")
{
<span class="text-light">@Model.ChatHistory[i].Name</span><span> &mdash; @message.Substring(0, Math.Min(65, message.Length)) </span><br />
}
}
}
</div>

View File

@ -38,6 +38,15 @@
{ {
<li class="nav-item text-center text-md-left"><a href="@ViewBag.DiscordLink" class="nav-link" target="_blank">Discord</a></li> <li class="nav-item text-center text-md-left"><a href="@ViewBag.DiscordLink" class="nav-link" target="_blank">Discord</a></li>
} }
@if (ViewBag.Authorized)
{
<li class="nav-item text-center text-md-left">@Html.ActionLink("", "ProfileAsync", "Client", new { id = ViewBag.User.ClientId }, new { @class = "nav-link oi oi-person oi-fix-navbar w-100", title = "Client Profile" })</li>
}
else
{
<li class="nav-item text-center text-md-left"><a href="#" id="profile_action_login_btn" class="nav-link profile-action oi oi-key oi-fix-navbar w-100" title="Login" data-action="login" aria-hidden="true"></a></li>
}
</ul> </ul>
<form class="form-inline text-primary pt-3 pb-3" method="get" action="/Client/FindAsync"> <form class="form-inline text-primary pt-3 pb-3" method="get" action="/Client/FindAsync">
<input id="client_search" name="clientName" class="form-control mr-auto ml-auto mr-md-2" type="text" placeholder="Find Player" /> <input id="client_search" name="clientName" class="form-control mr-auto ml-auto mr-md-2" type="text" placeholder="Find Player" />
@ -62,7 +71,6 @@
</div> </div>
</div> </div>
<!-- End Main Modal --> <!-- End Main Modal -->
<!-- Action Modal --> <!-- Action Modal -->
<div class="modal fade" id="actionModal" tabindex="-1" role="dialog" aria-labelledby="actionModalLabel" aria-hidden="true"> <div class="modal fade" id="actionModal" tabindex="-1" role="dialog" aria-labelledby="actionModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
@ -97,6 +105,7 @@
<script type="text/javascript" src="~/lib/moment-timezone/builds/moment-timezone-with-data.js"></script> <script type="text/javascript" src="~/lib/moment-timezone/builds/moment-timezone-with-data.js"></script>
<!--<script type="text/javascript" src="~/lib/popper.js/dist/popper.js"></script>--> <!--<script type="text/javascript" src="~/lib/popper.js/dist/popper.js"></script>-->
<script type="text/javascript" src="~/lib/bootstrap/dist/js/bootstrap.js"></script> <script type="text/javascript" src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script type="text/javascript" src="~/js/global.js"></script>
</environment> </environment>
<environment names="Production"> <environment names="Production">
<script type="text/javascript" src="~/lib/jQuery/dist/jquery.min.js"></script> <script type="text/javascript" src="~/lib/jQuery/dist/jquery.min.js"></script>
@ -104,6 +113,7 @@
<script type="text/javascript" src="~/lib/moment-timezone/builds/moment-timezone.min.js"></script> <script type="text/javascript" src="~/lib/moment-timezone/builds/moment-timezone.min.js"></script>
<!--<script type="text/javascript" src="~/lib/popper.js/dist/popper.min.js"></script>--> <!--<script type="text/javascript" src="~/lib/popper.js/dist/popper.min.js"></script>-->
<script type="text/javascript" src="~/lib/bootstrap/dist/js/bootstrap.min.js"></script> <script type="text/javascript" src="~/lib/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="~/js/global.js"></script>
</environment> </environment>
@RenderSection("scripts", required: false) @RenderSection("scripts", required: false)
</body> </body>

View File

@ -5,10 +5,20 @@
<PreserveCompilationContext>true</PreserveCompilationContext> <PreserveCompilationContext>true</PreserveCompilationContext>
<AssemblyName>IW4MAdmin</AssemblyName> <AssemblyName>IW4MAdmin</AssemblyName>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<PackageId>WebfrontCore</PackageId> <PackageId>RaidMax.IW4MAdmin.WebfrontCore</PackageId>
<Platforms>AnyCPU;x86</Platforms> <Platforms>AnyCPU;x86</Platforms>
<ApplicationIcon>wwwroot\favicon.ico</ApplicationIcon> <ApplicationIcon>wwwroot\favicon.ico</ApplicationIcon>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<Version>1.6.0</Version>
<Authors>RaidMax</Authors>
<Company>ForeverNone</Company>
<Copyright>2018</Copyright>
<PackageLicenseUrl>https://github.com/RaidMax/IW4M-Admin/blob/master/LICENSE</PackageLicenseUrl>
<Description>Complete administration tool designed for IW4x and compatible with most Call Of Duty® dedicated servers</Description>
<PackageProjectUrl>https://raidmax.org/IW4Madmin/</PackageProjectUrl>
<PackageIconUrl>https://raidmax.org/IW4Madmin/img/iw4adminicon-3.png</PackageIconUrl>
<RepositoryUrl>https://github.com/RaidMax/IW4M-Admin</RepositoryUrl>
<NeutralLanguage>en</NeutralLanguage>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@ -23,6 +33,7 @@
<Content Remove="bower.json" /> <Content Remove="bower.json" />
<Content Remove="bundleconfig.json" /> <Content Remove="bundleconfig.json" />
<Content Remove="compilerconfig.json" /> <Content Remove="compilerconfig.json" />
<Content Remove="web.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -32,6 +43,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="1.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="1.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="1.1.5" /> <PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="1.1.5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.7" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.7" />
<PackageReference Include="Microsoft.AspNetCore.Routing" Version="1.1.2" /> <PackageReference Include="Microsoft.AspNetCore.Routing" Version="1.1.2" />
@ -64,6 +77,7 @@
<None Include="bower.json" /> <None Include="bower.json" />
<None Include="bundleconfig.json" /> <None Include="bundleconfig.json" />
<None Include="compilerconfig.json" /> <None Include="compilerconfig.json" />
<None Include="web.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -78,6 +92,10 @@
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Views\Account\" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent"> <Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="if not &quot;$(SolutionDir)&quot;==&quot;*Undefined*&quot; (&#xD;&#xA; xcopy /Y &quot;$(SolutionDir)BUILD\Plugins&quot; &quot;$(TargetDir)Plugins\&quot;&#xD;&#xA;)" /> <Exec Command="if not &quot;$(SolutionDir)&quot;==&quot;*Undefined*&quot; (&#xD;&#xA; xcopy /Y &quot;$(SolutionDir)BUILD\Plugins&quot; &quot;$(TargetDir)Plugins\&quot;&#xD;&#xA;)" />
</Target> </Target>

View File

@ -42,7 +42,7 @@ a.nav-link {
.server-activity, .server-activity,
#mobile_seperator, #mobile_seperator,
.border-bottom { .border-bottom {
border-bottom: 1px solid $primary !important; border-bottom: 2px solid $primary !important;
} }
#client_search { #client_search {
@ -92,3 +92,9 @@ a.link-inverse:hover {
form * { form * {
border-radius: 0 !important; border-radius: 0 !important;
} }
.oi-fix-navbar {
line-height: 1.5 !important;
top: 0 !important;
font-size: 1rem !important;
}

BIN
WebfrontCore/wwwroot/js.zip Normal file

Binary file not shown.

View File

@ -0,0 +1,46 @@
$(document).ready(function () {
/*
* handle action modal
*/
$('.profile-action').click(function (e) {
const actionType = $(this).data('action');
$.get('/Action/' + actionType + 'Form')
.done(function (response) {
$('#actionModal .modal-body').html(response);
$('#actionModal').modal();
})
.fail(function (jqxhr, textStatus, error) {
$('#actionModal .modal-body').html('<span class="text-danger">' + error + '</span>');
$('#actionModal').modal();
});
});
/*
* handle action submit
*/
$(document).on('submit', '.action-form', function (e) {
e.preventDefault();
$(this).append($('#target_id input'));
const data = $(this).serialize();
$.get($(this).attr('action') + '/?' + data)
.done(function (response) {
// success without content
if (response.length === 0) {
location.reload();
}
else {
$('#actionModal .modal-body').html(response);
$('#actionModal').modal();
}
})
.fail(function (jqxhr, textStatus, error) {
if (jqxhr.status == 401) {
$('#actionModal .modal-body').removeClass('text-danger');
$('#actionModal .modal-body').prepend('<div class="text-danger mb-3">Invalid login credentials</div>');
}
else {
$('#actionModal .modal-body').html('<span class="text-danger">Error &mdash; ' + error + '</span>');
}
});
});
});

View File

@ -56,14 +56,41 @@ $(document).ready(function () {
$('.ip-locate-link').click(function (e) { $('.ip-locate-link').click(function (e) {
e.preventDefault(); e.preventDefault();
const ip = $(this).data("ip"); const ip = $(this).data("ip");
$.getJSON("http://ip-api.com/json/" + ip) $.getJSON('https://extreme-ip-lookup.com/json/' + ip)
.done(function (response) { .done(function (response) {
$('#mainModal .modal-title').text(ip); $('#mainModal .modal-title').text(ip);
$('#mainModal .modal-body').text(""); $('#mainModal .modal-body').text("");
$('#mainModal .modal-body').append("ASN &mdash; " + response["as"] + "<br/>"); if (response.ipName.length > 0) {
$('#mainModal .modal-body').append("ISP &mdash; " + response["isp"] + "<br/>"); $('#mainModal .modal-body').append("Hostname &mdash; " + response.ipName + '<br/>');
$('#mainModal .modal-body').append("Organization &mdash; " + response["org"] + "<br/>"); }
$('#mainModal .modal-body').append("Location &mdash; " + response["city"] + ", " + response["regionName"] + ", " + response["country"] + "<br/>"); if (response.isp.length > 0) {
$('#mainModal .modal-body').append("ISP &mdash; " + response.isp + '<br/>');
}
if (response.ipType.length > 0) {
$('#mainModal .modal-body').append("Type &mdash; " + response.ipType + '<br/>');
}
if (response.org.length > 0) {
$('#mainModal .modal-body').append("Organization &mdash; " + response.org + '<br/>');
}
if (response['businessName'].length > 0) {
$('#mainModal .modal-body').append("Business &mdash; " + response.businessName + '<br/>');
}
if (response['businessWebsite'].length > 0) {
$('#mainModal .modal-body').append("Website &mdash; " + response.businessWebsite + '<br/>');
}
if (response.city.length > 0 || response.region.length > 0 || response.country.length > 0) {
$('#mainModal .modal-body').append("Location &mdash; ");
}
if (response.city.length > 0) {
$('#mainModal .modal-body').append(response.city);
}
if (response.region.length > 0) {
$('#mainModal .modal-body').append(', ' + response.region);
}
if (response.country.length > 0) {
$('#mainModal .modal-body').append(', ' + response.country);
}
$('#mainModal').modal(); $('#mainModal').modal();
}) })
.fail(function (jqxhr, textStatus, error) { .fail(function (jqxhr, textStatus, error) {
@ -72,39 +99,6 @@ $(document).ready(function () {
$('#mainModal').modal(); $('#mainModal').modal();
}); });
}); });
/*
* handle action modal
*/
$('.profile-action').click(function (e) {
const actionType = $(this).data('action');
$.get('/Action/' + actionType + 'Form')
.done(function (response) {
$('#actionModal .modal-body').html(response);
$('#actionModal').modal();
})
.fail(function (jqxhr, textStatus, error) {
$('#actionModal .modal-body').html('<span class="text-danger">' + error + '</span>');
$('#actionModal').modal();
});
});
/*
* handle action submit
*/
$(document).on('submit', '.action-form', function (e) {
e.preventDefault();
$(this).append($('#target_id input'));
const data = $(this).serialize();
$.get($(this).attr('action') + '/?' + data)
.done(function (response) {
$('#actionModal .modal-body').html(response);
$('#actionModal').modal();
})
.fail(function (jqxhr, textStatus, error) {
$('#actionModal .modal-body').html('<span class="text-danger">Error' + error + '</span>');
});
});
}); });
function penaltyToName(penaltyName) { function penaltyToName(penaltyName) {
@ -179,7 +173,7 @@ function loadMeta(meta) {
} }
} }
else if (meta.key.includes("Alias")) { else if (meta.key.includes("Alias")) {
eventString = `<div><span class="text-primary">${meta.value}</span></div>`; eventString = `<div><span class="text-success">${meta.value}</span></div>`;
} }
// it's a message // it's a message
else if (meta.key.includes("Event")) { else if (meta.key.includes("Event")) {

View File

@ -6058,7 +6058,7 @@ a.nav-link {
.server-activity, .server-activity,
#mobile_seperator, #mobile_seperator,
.border-bottom { .border-bottom {
border-bottom: 1px solid #007ACC !important; } border-bottom: 2px solid #007ACC !important; }
#client_search { #client_search {
background-color: #222222 !important; background-color: #222222 !important;
@ -6097,3 +6097,8 @@ a.link-inverse:hover {
form * { form * {
border-radius: 0 !important; } border-radius: 0 !important; }
.oi-fix-navbar {
line-height: 1.5 !important;
top: 0 !important;
font-size: 1rem !important; }

File diff suppressed because one or more lines are too long