diff --git a/.gitignore b/.gitignore index 2ba5cbe26..3ea9b355f 100644 --- a/.gitignore +++ b/.gitignore @@ -221,4 +221,6 @@ DEPLOY global.min.css global.min.js bootstrap-custom.css -bootstrap-custom.min.css \ No newline at end of file +bootstrap-custom.min.css +**/Master/static +**/Master/dev_env \ No newline at end of file diff --git a/Application/API/Master/ApiInstance.cs b/Application/API/Master/ApiInstance.cs new file mode 100644 index 000000000..846adb744 --- /dev/null +++ b/Application/API/Master/ApiInstance.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json; +using RestEase; + +namespace IW4MAdmin.Application.API.Master +{ + public class ApiInstance + { + [JsonProperty("id")] + public string Id { get; set; } + [JsonProperty("uptime")] + public int Uptime { get; set; } + [JsonProperty("version")] + public float Version { get; set; } + [JsonProperty("servers")] + public List Servers { get; set; } + } +} diff --git a/Application/API/Master/ApiServer.cs b/Application/API/Master/ApiServer.cs new file mode 100644 index 000000000..41637ebaf --- /dev/null +++ b/Application/API/Master/ApiServer.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace IW4MAdmin.Application.API.Master +{ + public class ApiServer + { + [JsonProperty("id")] + public int Id { get; set; } + [JsonProperty("port")] + public short Port { get; set; } + [JsonProperty("gametype")] + public string Gametype { get; set; } + [JsonProperty("map")] + public string Map { get; set; } + [JsonProperty("game")] + public string Game { get; set; } + [JsonProperty("hostname")] + public string Hostname { get; set; } + [JsonProperty("clientnum")] + public int ClientNum { get; set; } + [JsonProperty("maxclientnum")] + public int MaxClientNum { get; set; } + } +} diff --git a/Application/API/Master/Heartbeat.cs b/Application/API/Master/Heartbeat.cs new file mode 100644 index 000000000..2666a494e --- /dev/null +++ b/Application/API/Master/Heartbeat.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RestEase; +using SharedLibraryCore; + +namespace IW4MAdmin.Application.API.Master +{ + public class Heartbeat + { + static IMasterApi api; + + public static async Task Send(ApplicationManager mgr, bool firstHeartbeat = false) + { + + if (firstHeartbeat) + { + api = RestClient.For("http://127.0.0.1"); + + var token = await api.Authenticate(new AuthenticationId() + { + Id = mgr.GetApplicationSettings().Configuration().Id + }); + + api.AuthorizationToken = $"Bearer {token.AccessToken}"; + } + + var instance = new ApiInstance() + { + Id = mgr.GetApplicationSettings().Configuration().Id, + Uptime = (int)(DateTime.UtcNow - mgr.StartTime).TotalSeconds, + Version = (float)Program.Version, + Servers = mgr.Servers.Select(s => + new ApiServer() + { + ClientNum = s.ClientNum, + Game = s.GameName.ToString(), + Gametype = s.Gametype, + Hostname = s.Hostname, + Map = s.CurrentMap.Name, + MaxClientNum = s.MaxClients, + Id = s.GetHashCode(), + Port = (short)s.GetPort() + }).ToList() + }; + + if (firstHeartbeat) + { + instance = await api.AddInstance(instance); + } + + else + { + instance = await api.UpdateInstance(instance.Id, instance); + } + } + } +} diff --git a/Application/API/Master/IMasterApi.cs b/Application/API/Master/IMasterApi.cs new file mode 100644 index 000000000..2609b4f38 --- /dev/null +++ b/Application/API/Master/IMasterApi.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using RestEase; + +namespace IW4MAdmin.Application.API.Master +{ + public class AuthenticationId + { + [JsonProperty("id")] + public string Id { get; set; } + } + + public class TokenId + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + } + + [Header("User-Agent", "IW4MAdmin-RestEase")] + public interface IMasterApi + { + [Header("Authorization")] + string AuthorizationToken { get; set; } + + [Post("authenticate")] + Task Authenticate([Body] AuthenticationId Id); + + [Post("instance/")] + Task AddInstance([Body] ApiInstance instance); + + [Put("instance/{id}")] + Task UpdateInstance([Path] string id, [Body] ApiInstance instance); + } +} diff --git a/Application/Application.csproj b/Application/Application.csproj index 681fdcb5e..d13e54b39 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -30,6 +30,10 @@ + + + + true diff --git a/Application/EventParsers/T6MEventParser.cs b/Application/EventParsers/T6MEventParser.cs index ee51e5b40..3b42c3043 100644 --- a/Application/EventParsers/T6MEventParser.cs +++ b/Application/EventParsers/T6MEventParser.cs @@ -51,7 +51,7 @@ namespace Application.EventParsers }; } - if (lineSplit[0].Contains("ShutdownGame")) + if (lineSplit[0].Contains("ExitLevel")) { return new GameEvent() { @@ -69,6 +69,11 @@ namespace Application.EventParsers }; } + /*if (lineSplit[0].Contains("ShutdownGame")) + { + + }*/ + if (lineSplit[0].Contains("InitGame")) { return new GameEvent() diff --git a/Application/Main.cs b/Application/Main.cs index 0d83c2569..568998efc 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -65,6 +65,7 @@ namespace IW4MAdmin.Application { Task.Run(() => WebfrontCore.Program.Init(ServerManager)); } + ServerManager.Start(); ServerManager.Logger.WriteVerbose("Shutdown complete"); diff --git a/Application/Manager.cs b/Application/Manager.cs index 8a7f2a62a..8ecf8e6b2 100644 --- a/Application/Manager.cs +++ b/Application/Manager.cs @@ -29,6 +29,7 @@ namespace IW4MAdmin.Application public ILogger Logger { get; private set; } public bool Running { get; private set; } public EventHandler ServerEventOccurred { get; private set; } + public DateTime StartTime { get; private set; } static ApplicationManager Instance; List TaskStatuses; @@ -60,6 +61,7 @@ namespace IW4MAdmin.Application ServerEventOccurred += Api.OnServerEvent; ConfigHandler = new BaseConfigurationHandler("IW4MAdminSettings"); Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey); + StartTime = DateTime.UtcNow; } private void OnCancelKey(object sender, ConsoleCancelEventArgs args) @@ -133,6 +135,15 @@ namespace IW4MAdmin.Application await ConfigHandler.Save(); } + else if(config != null) + { + if (string.IsNullOrEmpty(config.Id)) + { + config.Id = Guid.NewGuid().ToString(); + await ConfigHandler.Save(); + } + } + else if (config.Servers.Count == 0) throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid"); @@ -244,8 +255,71 @@ namespace IW4MAdmin.Application Running = true; } + private void HeartBeatThread() + { + bool successfulConnection = false; + restartConnection: + while (!successfulConnection) + { + try + { + API.Master.Heartbeat.Send(this, true).Wait(); + successfulConnection = true; + } + + catch (Exception e) + { + successfulConnection = false; + Logger.WriteWarning($"Could not connect to heartbeat server - {e.Message}"); + } + + Thread.Sleep(30000); + } + + while (Running) + { + Logger.WriteDebug("Sending heartbeat..."); + try + { + API.Master.Heartbeat.Send(this).Wait(); + } + catch (System.Net.Http.HttpRequestException e) + { + Logger.WriteWarning($"Could not send heartbeat - {e.Message}"); + } + + catch (AggregateException e) + { + Logger.WriteWarning($"Could not send heartbeat - {e.Message}"); + var exceptions = e.InnerExceptions.Where(ex => ex.GetType() == typeof(RestEase.ApiException)); + + foreach (var ex in exceptions) + { + if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + successfulConnection = false; + goto restartConnection; + } + } + } + + catch (RestEase.ApiException e) + { + Logger.WriteWarning($"Could not send heartbeat - {e.Message}"); + if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + successfulConnection = false; + goto restartConnection; + } + } + + Thread.Sleep(30000); + } + } + public void Start() { + Task.Run(() => HeartBeatThread()); while (Running || TaskStatuses.Count > 0) { for (int i = 0; i < TaskStatuses.Count; i++) diff --git a/Application/Server.cs b/Application/Server.cs index c82637f56..d029a1172 100644 --- a/Application/Server.cs +++ b/Application/Server.cs @@ -630,6 +630,7 @@ namespace IW4MAdmin this.CurrentMap = Maps.Find(m => m.Name == mapname.Value) ?? new Map() { Alias = mapname.Value, Name = mapname.Value }; this.MaxClients = maxplayers.Value; this.FSGame = game.Value; + this.Gametype = (await this.GetDvarAsync("g_gametype")).Value; await this.SetDvarAsync("sv_kickbantime", 60); @@ -647,7 +648,7 @@ namespace IW4MAdmin CustomCallback = await ScriptLoaded(); string mainPath = EventParser.GetGameDir(); #if DEBUG - // basepath.Value = @"\\192.168.88.253\Call of Duty Black Ops II"; + basepath.Value = @"\\192.168.88.253\Call of Duty Black Ops II"; #endif string logPath = game.Value == string.Empty ? $"{basepath.Value.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{mainPath}{Path.DirectorySeparatorChar}{logfile.Value}" : diff --git a/IW4MAdmin.sln b/IW4MAdmin.sln index 2e76d4cf0..429223e03 100644 --- a/IW4MAdmin.sln +++ b/IW4MAdmin.sln @@ -26,7 +26,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Welcome", "Plugins\Welcome\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProfanityDeterment", "Plugins\ProfanityDeterment\ProfanityDeterment.csproj", "{958FF7EC-0226-4E85-A85B-B84EC768197D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Login", "Plugins\Login\Login.csproj", "{D9F2ED28-6FA5-40CA-9912-E7A849147AB1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Login", "Plugins\Login\Login.csproj", "{D9F2ED28-6FA5-40CA-9912-E7A849147AB1}" +EndProject +Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "Master", "Master\Master.pyproj", "{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -168,6 +170,22 @@ Global {D9F2ED28-6FA5-40CA-9912-E7A849147AB1}.Release|x64.Build.0 = Release|Any CPU {D9F2ED28-6FA5-40CA-9912-E7A849147AB1}.Release|x86.ActiveCfg = Release|Any CPU {D9F2ED28-6FA5-40CA-9912-E7A849147AB1}.Release|x86.Build.0 = Release|Any CPU + {F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Debug|x64.ActiveCfg = Debug|Any CPU + {F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Debug|x64.Build.0 = Debug|Any CPU + {F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Debug|x86.ActiveCfg = Debug|Any CPU + {F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Debug|x86.Build.0 = Debug|Any CPU + {F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Release|Any CPU.Build.0 = Release|Any CPU + {F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Release|x64.ActiveCfg = Release|Any CPU + {F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Release|x64.Build.0 = Release|Any CPU + {F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Release|x86.ActiveCfg = Release|Any CPU + {F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Master/Master.pyproj b/Master/Master.pyproj new file mode 100644 index 000000000..f7b9a4ecd --- /dev/null +++ b/Master/Master.pyproj @@ -0,0 +1,166 @@ + + + + 10.0 + Debug + 2.0 + f5051a32-6bd0-4128-abba-c202ee15fc5c + . + {789894c7-04a9-4a11-a6b5-3f4435165112};{1b580a1a-fdb3-4b32-83e1-6407eb2722e6};{349c5851-65df-11da-9384-00065b846f21};{888888a0-9f3d-457c-b088-3a5042f75d52} + runserver.py + + + . + Web launcher + http://localhost + . + true + Master + Master + MSBuild|dev_env|$(MSBuildProjectFullPath) + + + true + false + + + true + false + + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dev_env + 3.6 + dev_env (Python 3.6 (64-bit)) + Scripts\python.exe + Scripts\pythonw.exe + PYTHONPATH + X64 + + + + + + + + + + + + + True + True + http://localhost + False + + + + + + + CurrentPage + True + False + False + False + + + + + + + + + False + False + + + + + \ No newline at end of file diff --git a/Master/master/__init__.py b/Master/master/__init__.py new file mode 100644 index 000000000..444b9e377 --- /dev/null +++ b/Master/master/__init__.py @@ -0,0 +1,17 @@ +""" +The flask application package. +""" + +from flask import Flask +from flask_restful import Resource, Api +from flask_jwt_extended import JWTManager +from master.context.base import Base + +app = Flask(__name__) +app.config['JWT_SECRET_KEY'] = 'my key!' +jwt = JWTManager(app) +api = Api(app) +ctx = Base() + +import master.routes +import master.views diff --git a/Master/master/context/__init__.py b/Master/master/context/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/Master/master/context/__init__.py @@ -0,0 +1 @@ + diff --git a/Master/master/context/base.py b/Master/master/context/base.py new file mode 100644 index 000000000..c2d973755 --- /dev/null +++ b/Master/master/context/base.py @@ -0,0 +1,67 @@ +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.interval import IntervalTrigger +import time + +class Base(): + def __init__(self): + self.instance_list = {} + self.server_list = {} + self.token_list = {} + self.scheduler = BackgroundScheduler() + self.scheduler.start() + self.scheduler.add_job( + func=self._remove_staleinstances, + trigger=IntervalTrigger(seconds=120), + id='stale_instance_remover', + name='Remove stale instances if no heartbeat in 120 seconds', + replace_existing=True + ) + + def _remove_staleinstances(self): + for key, value in list(self.instance_list.items()): + if int(time.time()) - value.last_heartbeat > 120: + print('[_remove_staleinstances] removing stale instance {id}'.format(id=key)) + del self.instance_list[key] + del self.token_list[key] + print('[_remove_staleinstances] {count} active instances'.format(count=len(self.instance_list))) + + def get_server_count(self): + return self.server_list.count + + def get_instance_count(self): + return self.instance_list.count + + def get_instance(self, id): + return self.instance_list[id] + + def instance_exists(self, instance_id): + if instance_id in self.instance_list.keys(): + return instance_id + else: + False + + def add_instance(self, instance): + if instance.id in self.instance_list: + print('[add_instance] instance {id} already added, updating instead'.format(id=instance.id)) + return self.update_instance(instance) + else: + print('[add_instance] adding instance {id}'.format(id=instance.id)) + self.instance_list[instance.id] = instance + + def update_instance(self, instance): + if instance.id not in self.instance_list: + print('[update_instance] instance {id} not added, adding instead'.format(id=instance.id)) + return self.add_instance(instance) + else: + print('[update_instance] updating instance {id}'.format(id=instance.id)) + self.instance_list[instance.id] = instance + + def add_token(self, instance_id, token): + print('[add_token] adding {token} for id {id}'.format(token=token, id=instance_id)) + self.token_list[instance_id] = token + + def get_token(self, instance_id): + try: + return self.token_list[instance_id] + except KeyError: + return False \ No newline at end of file diff --git a/Master/master/models/__init__.py b/Master/master/models/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/Master/master/models/__init__.py @@ -0,0 +1 @@ + diff --git a/Master/master/models/instancemodel.py b/Master/master/models/instancemodel.py new file mode 100644 index 000000000..35f31a29e --- /dev/null +++ b/Master/master/models/instancemodel.py @@ -0,0 +1,12 @@ +import time + +class InstanceModel(object): + def __init__(self, id, version, uptime, servers): + self.id = id + self.version = version + self.uptime = uptime + self.servers = servers + self.last_heartbeat = int(time.time()) + + def __repr__(self): + return ''.format(id=self.id) \ No newline at end of file diff --git a/Master/master/models/servermodel.py b/Master/master/models/servermodel.py new file mode 100644 index 000000000..c81d2d0d8 --- /dev/null +++ b/Master/master/models/servermodel.py @@ -0,0 +1,14 @@ + +class ServerModel(object): + def __init__(self, id, port, game, hostname, clientnum, maxclientnum, map, gametype): + self.id = id + self.port = port + self.game = game + self.hostname = hostname + self.clientnum = clientnum + self.maxclientnum = maxclientnum + self.map = map + self.gametype = gametype + + def __repr__(self): + return ''.format(id=self.id) diff --git a/Master/master/resources/__init__.py b/Master/master/resources/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/Master/master/resources/__init__.py @@ -0,0 +1 @@ + diff --git a/Master/master/resources/authenticate.py b/Master/master/resources/authenticate.py new file mode 100644 index 000000000..a0711332c --- /dev/null +++ b/Master/master/resources/authenticate.py @@ -0,0 +1,16 @@ +from flask_restful import Resource +from flask import request, jsonify +from flask_jwt_extended import create_access_token +from master import app, ctx + + +class Authenticate(Resource): + def post(self): + instance_id = request.json['id'] + if ctx.get_token(instance_id) is not False: + return { 'message' : 'that id already has a token'}, 401 + else: + token = create_access_token(instance_id) + ctx.add_token(instance_id, token) + return { 'access_token' : token }, 200 + \ No newline at end of file diff --git a/Master/master/resources/instance.py b/Master/master/resources/instance.py new file mode 100644 index 000000000..cb1882d2d --- /dev/null +++ b/Master/master/resources/instance.py @@ -0,0 +1,37 @@ +from flask_restful import Resource +from flask import request +from flask_jwt_extended import jwt_required +from marshmallow import ValidationError +from master.schema.instanceschema import InstanceSchema +from master import ctx + +class Instance(Resource): + def get(self, id=None): + if id is None: + schema = InstanceSchema(many=True) + instances = schema.dump(ctx.instance_list.values()) + return instances + else: + try: + instance = ctx.get_instance(id) + return InstanceSchema().dump(instance) + except KeyError: + return {'message' : 'instance not found'}, 404 + + @jwt_required + def put(self, id): + try: + instance = InstanceSchema().load(request.json) + except ValidationError as err: + return {'message' : err.messages }, 400 + ctx.update_instance(instance) + return InstanceSchema().dump(instance) + + @jwt_required + def post(self): + try: + instance = InstanceSchema().load(request.json) + except ValidationError as err: + return err.messages + ctx.add_instance(instance) + return InstanceSchema().dump(instance) diff --git a/Master/master/resources/null.py b/Master/master/resources/null.py new file mode 100644 index 000000000..9c3a7b627 --- /dev/null +++ b/Master/master/resources/null.py @@ -0,0 +1,11 @@ +from flask_restful import Resource +from master.models.servermodel import ServerModel +from master.schema.serverschema import ServerSchema +from master.models.instancemodel import InstanceModel +from master.schema.instanceschema import InstanceSchema + +class Null(Resource): + def get(self): + server = ServerModel(1, 'T6M', 'test', 0, 18, 'mp_test', 'tdm') + instance = InstanceModel(1, 1.5, 132, [server]) + return InstanceSchema().dump(instance) diff --git a/Master/master/routes.py b/Master/master/routes.py new file mode 100644 index 000000000..2c5517254 --- /dev/null +++ b/Master/master/routes.py @@ -0,0 +1,10 @@ +from master import api + +from master.resources.null import Null +from master.resources.instance import Instance +from master.resources.authenticate import Authenticate + +api.add_resource(Null, '/null') +api.add_resource(Instance, '/instance/', '/instance/') + +api.add_resource(Authenticate, '/authenticate') \ No newline at end of file diff --git a/Master/master/schema/__init__.py b/Master/master/schema/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/Master/master/schema/__init__.py @@ -0,0 +1 @@ + diff --git a/Master/master/schema/instanceschema.py b/Master/master/schema/instanceschema.py new file mode 100644 index 000000000..fc2410f05 --- /dev/null +++ b/Master/master/schema/instanceschema.py @@ -0,0 +1,29 @@ +from marshmallow import Schema, fields, post_load, validate +from master.models.instancemodel import InstanceModel +from master.schema.serverschema import ServerSchema + +class InstanceSchema(Schema): + id = fields.String( + required=True + ) + version = fields.Float( + required=True, + validate=validate.Range(1.0, 10.0, 'invalid version number') + ) + servers = fields.Nested( + ServerSchema, + many=True, + validate=validate.Length(0, 32, 'invalid server count') + ) + uptime = fields.Int( + required=True, + validate=validate.Range(0, 2147483647, 'invalid uptime') + ) + last_heartbeat = fields.Int( + required=False + ) + + @post_load + def make_instance(self, data): + return InstanceModel(**data) + diff --git a/Master/master/schema/serverschema.py b/Master/master/schema/serverschema.py new file mode 100644 index 000000000..81e6d3e44 --- /dev/null +++ b/Master/master/schema/serverschema.py @@ -0,0 +1,40 @@ +from marshmallow import Schema, fields, post_load, validate +from master.models.servermodel import ServerModel + +class ServerSchema(Schema): + id = fields.Int( + required=True, + validate=validate.Range(1, 2147483647, 'invalid id') + ) + port = fields.Int( + required=True, + validate=validate.Range(1, 665535, 'invalid port') + ) + game = fields.String( + required=True, + validate=validate.Length(1, 8, 'invalid game name') + ) + hostname = fields.String( + required=True, + validate=validate.Length(1, 48, 'invalid hostname') + ) + clientnum = fields.Int( + required=True, + validate=validate.Range(0, 128, 'invalid clientnum') + ) + maxclientnum = fields.Int( + required=True, + validate=validate.Range(1, 128, 'invalid maxclientnum') + ) + map = fields.String( + required=True, + validate=validate.Length(1, 32, 'invalid map name') + ) + gametype = fields.String( + required=True, + validate=validate.Length(1, 16, 'invalid gametype') + ) + + @post_load + def make_instance(self, data): + return ServerModel(**data) \ No newline at end of file diff --git a/Master/master/templates/index.html b/Master/master/templates/index.html new file mode 100644 index 000000000..5c9f5a162 --- /dev/null +++ b/Master/master/templates/index.html @@ -0,0 +1,5 @@ +{% extends "layout.html" %} + +{% block content %} + +{% endblock %} diff --git a/Master/master/templates/layout.html b/Master/master/templates/layout.html new file mode 100644 index 000000000..aa1843f4e --- /dev/null +++ b/Master/master/templates/layout.html @@ -0,0 +1,44 @@ + + + + + + IW4MAdmin Master | {{ title }} + + + + + + + + +
+ {% block content %}{% endblock %} +
+
+
+
+ + + + + {% block scripts %}{% endblock %} + + + diff --git a/Master/master/views.py b/Master/master/views.py new file mode 100644 index 000000000..7b27e6a8e --- /dev/null +++ b/Master/master/views.py @@ -0,0 +1,17 @@ +""" +Routes and views for the flask application. +""" + +from datetime import datetime +from flask import render_template +from master import app + +@app.route('/') +@app.route('/home') +def home(): + """Renders the home page.""" + return render_template( + 'index.html', + title='Home Page', + year=datetime.now().year, + ) \ No newline at end of file diff --git a/Master/requirements.txt b/Master/requirements.txt new file mode 100644 index 000000000..a0c8750b5 --- /dev/null +++ b/Master/requirements.txt @@ -0,0 +1,16 @@ +aniso8601==3.0.0 +click==6.7 +Flask==0.12.2 +Flask-JWT==0.3.2 +Flask-JWT-Extended==3.8.1 +Flask-RESTful==0.3.6 +itsdangerous==0.24 +Jinja2==2.10 +MarkupSafe==1.0 +marshmallow==3.0.0b8 +pip==9.0.1 +PyJWT==1.4.2 +pytz==2018.4 +setuptools==39.0.1 +six==1.11.0 +Werkzeug==0.14.1 diff --git a/Master/runserver.py b/Master/runserver.py new file mode 100644 index 000000000..eb9215171 --- /dev/null +++ b/Master/runserver.py @@ -0,0 +1,9 @@ +""" +This script runs the Master application using a development server. +""" + +from os import environ +from master import app + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=80, debug=True) diff --git a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs index e7a5b796c..d132abee6 100644 --- a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs +++ b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs @@ -16,6 +16,7 @@ namespace SharedLibraryCore.Configuration public string CustomSayName { get; set; } public string DiscordInviteCode { get; set; } public string IPHubAPIKey { get; set; } + public string Id { get; set; } public List Servers { get; set; } public int AutoMessagePeriod { get; set; } public List AutoMessages { get; set; } @@ -24,6 +25,7 @@ namespace SharedLibraryCore.Configuration public IBaseConfiguration Generate() { + Id = Guid.NewGuid().ToString(); EnableWebFront = Utilities.PromptBool("Enable webfront"); EnableMultipleOwners = Utilities.PromptBool("Enable multiple owners"); EnableSteppedHierarchy = Utilities.PromptBool("Enable stepped privilege hierarchy");