From f6763b749b2e3c3b31a5e23b42c6f81e4069e70f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Borgsm=C3=BCller?= Date: Sun, 17 Jul 2022 20:53:57 +0200 Subject: [PATCH 1/4] Add open port checking, update LiteNetLib --- src/api/CSM.API.csproj | 4 +- src/basegame/CSM.BaseGame.csproj | 4 +- src/csm/CSM.csproj | 7 ++-- src/csm/Networking/IpAddress.cs | 65 +++++++++++++++++++++++++++++-- src/csm/Networking/Server.cs | 64 ++++++++++++++++++++++++++++-- src/csm/Panels/ManageGamePanel.cs | 63 ++++++++++++++++++++++++++---- src/csm/Util/CSMWebClient.cs | 53 +++++++++++++++++++++++++ 7 files changed, 237 insertions(+), 23 deletions(-) create mode 100644 src/csm/Util/CSMWebClient.cs diff --git a/src/api/CSM.API.csproj b/src/api/CSM.API.csproj index 033e08ab..966af110 100644 --- a/src/api/CSM.API.csproj +++ b/src/api/CSM.API.csproj @@ -58,10 +58,10 @@ - 0.9.2.2 + 0.9.4 - 2.4.6 + 2.4.7 diff --git a/src/basegame/CSM.BaseGame.csproj b/src/basegame/CSM.BaseGame.csproj index 3d8bc904..4dc29d6e 100644 --- a/src/basegame/CSM.BaseGame.csproj +++ b/src/basegame/CSM.BaseGame.csproj @@ -229,10 +229,10 @@ - 0.9.2.2 + 0.9.4 - 2.0.0 + 2.1.0 diff --git a/src/csm/CSM.csproj b/src/csm/CSM.csproj index 93bcb604..e9457ef0 100644 --- a/src/csm/CSM.csproj +++ b/src/csm/CSM.csproj @@ -142,20 +142,21 @@ + - 0.9.2.2 + 0.9.4 2.1.0 - 2.4.6 + 2.4.7 - 2.0.0 + 2.1.0 diff --git a/src/csm/Networking/IpAddress.cs b/src/csm/Networking/IpAddress.cs index f39e1c6a..bf69cfcf 100644 --- a/src/csm/Networking/IpAddress.cs +++ b/src/csm/Networking/IpAddress.cs @@ -1,9 +1,26 @@ using System; +using System.IO; using System.Net; using System.Net.Sockets; +using System.ServiceModel.Channels; +using System.Text; +using CSM.API; +using CSM.Util; namespace CSM.Networking { + public struct PortState + { + public string message; + public HttpStatusCode status; + + public PortState(string message, HttpStatusCode status) + { + this.message = message; + this.status = status; + } + } + public static class IpAddress { private static string _localIp; @@ -22,7 +39,7 @@ public static string GetLocalIpAddress() using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0)) { //Connect to 8.8.8.8 (Google IP) - socket.Connect("8.8.8.8", 65530); + socket.Connect("csm-check.kaenganxt.dev", 65530); //Get the IPEndPoint IPEndPoint endPoint = socket.LocalEndPoint as IPEndPoint; //Get the IP Address (Internal) from the IPEndPoint @@ -46,15 +63,55 @@ public static string GetExternalIpAddress() try { - //Get the External IP (IPv4) Address from internet - _externalIp = new WebClient().DownloadString("http://api.ipify.org"); // HTTPS doesn't work + //Get the External IP address from internet + _externalIp = new CSMWebClient().DownloadString("http://csm-check.kaenganxt.dev/api/ip"); return _externalIp; } - catch (Exception) + catch (Exception e) { //On error return "Not found" + Log.Error("Failed to request IP: " + e.Message); return "Not found"; } } + + public static PortState CheckPort(int port) + { + CSMWebClient client = new CSMWebClient(); + try + { + string answer = client.DownloadString("http://csm-check.kaenganxt.dev/api/check?port=" + port); + return new PortState(answer, client.StatusCode()); + } + catch (WebException e) + { + if (e.Response is HttpWebResponse response) + { + Encoding encoding = response.CharacterSet != null ? Encoding.GetEncoding(response.CharacterSet) : Encoding.ASCII; + using (Stream stream = response.GetResponseStream()) + { + if (stream != null) + { + using (StreamReader reader = new StreamReader(stream, encoding)) + { + return new PortState(reader.ReadToEnd(), response.StatusCode); + } + } + else + { + return new PortState(e.Message, HttpStatusCode.InternalServerError); + } + } + } + else + { + return new PortState(e.Message, HttpStatusCode.InternalServerError); + } + } + catch (Exception e) + { + return new PortState(e.Message, HttpStatusCode.InternalServerError); + } + } } } diff --git a/src/csm/Networking/Server.cs b/src/csm/Networking/Server.cs index 4407a2c2..25de531c 100644 --- a/src/csm/Networking/Server.cs +++ b/src/csm/Networking/Server.cs @@ -43,6 +43,8 @@ public class Server /// public ServerStatus Status { get; private set; } + private bool automaticSuccess; + public Server() { // Set up network items @@ -94,12 +96,14 @@ public bool StartServer(ServerConfig serverConfig) nat.DiscoverDeviceAsync(PortMapper.Upnp, cts).ContinueWith(task => task.Result.CreatePortMapAsync(new Mapping(Protocol.Udp, Config.Port, Config.Port, "Cities Skylines Multiplayer (UDP)"))).Wait(); + automaticSuccess = true; } - catch (Exception e) + catch (Exception) { - Log.Error($"Failed to automatically open port. Manual Port Forwarding is required: {e.Message}"); - Chat.Instance.PrintGameMessage(Chat.MessageType.Error, "Failed to automatically open port. Manual port forwarding is required."); + automaticSuccess = false; } + + new Thread(CheckPort).Start(); // Update the status Status = ServerStatus.Running; @@ -111,10 +115,62 @@ public bool StartServer(ServerConfig serverConfig) // Update the console to let the user know the server is running Log.Info("The server has started."); - Chat.Instance.PrintGameMessage("The server has started."); + Chat.Instance.PrintGameMessage("The server has started. Checking if it is reachable from the internet..."); return true; } + private void CheckPort() + { + PortState state = IpAddress.CheckPort(Config.Port); + string message; + bool portOpen = false; + switch (state.status) + { + case HttpStatusCode.ServiceUnavailable: // Could not reach port + if (automaticSuccess) + { + message = + "Port was forwarded automatically, but server is not reachable from the internet."; + } + else + { + message = + "Port could not be forwarded automatically and server is not reachable from the internet. Manual port forwarding is required."; + } + break; + case HttpStatusCode.OK: // Success + portOpen = true; + if (automaticSuccess) + { + message = + "Port was forwarded automatically and server is reachable from the internet!"; + } + else + { + message = "Server is reachable from the internet!"; + } + break; + default: // Something failed + if (automaticSuccess) + { + message = "Port was forwarded automatically, but couldn't be checked due to error: " + + state.message; + } + else + { + message = "Port could not be forwarded automatically, and couldn't be checked due to error: " + + state.message; + } + break; + } + + if (!portOpen) + { + Log.Warn(message); + } + Chat.Instance.PrintGameMessage(portOpen ? Chat.MessageType.Normal : Chat.MessageType.Warning, message); + } + /// /// Stops the server /// diff --git a/src/csm/Panels/ManageGamePanel.cs b/src/csm/Panels/ManageGamePanel.cs index 476f17cf..4731ae80 100644 --- a/src/csm/Panels/ManageGamePanel.cs +++ b/src/csm/Panels/ManageGamePanel.cs @@ -1,6 +1,12 @@ -using ColossalFramework.UI; +using System; +using System.Net; +using System.Threading; +using ColossalFramework; +using ColossalFramework.UI; +using CSM.API; using CSM.Helpers; using CSM.Networking; +using CSM.Util; using UnityEngine; namespace CSM.Panels @@ -10,10 +16,12 @@ public class ManageGamePanel : UIPanel private UITextField _portField; private UITextField _localIpField; private UITextField _externalIpField; + private UILabel _portState; private UIButton _closeButton; - private string _portVal, _localIpVal, _externalIpVal; + private int _portVal; + private string _localIpVal, _externalIpVal; public override void Start() { @@ -28,18 +36,18 @@ public override void Start() relativePosition = PanelManager.GetCenterPosition(this); // Title Label - UILabel title = this.CreateTitleLabel("Manage Server", new Vector2(100, -20)); + this.CreateTitleLabel("Manage Server", new Vector2(100, -20)); // Port label this.CreateLabel("Port:", new Vector2(10, -75)); // Port field - _portVal = MultiplayerManager.Instance.CurrentServer.Config.Port.ToString(); - _portField = this.CreateTextField(_portVal, new Vector2(10, -100)); + _portVal = MultiplayerManager.Instance.CurrentServer.Config.Port; + _portField = this.CreateTextField(_portVal.ToString(), new Vector2(10, -100)); _portField.selectOnFocus = true; _portField.eventTextChanged += (ui, value) => { - _portField.text = _portVal; + _portField.text = _portVal.ToString(); }; // Local IP label @@ -65,6 +73,9 @@ public override void Start() { _externalIpField.text = _externalIpVal; }; + + _portState = this.CreateLabel("", new Vector2(10, -310)); + _portState.textAlignment = UIHorizontalAlignment.Center; // Close this dialog _closeButton = this.CreateButton("Close", new Vector2(10, -375)); @@ -73,14 +84,50 @@ public override void Start() isVisible = false; }; + new Thread(CheckPort).Start(); + eventVisibilityChanged += (component, visible) => { if (!visible) return; - _portVal = MultiplayerManager.Instance.CurrentServer.Config.Port.ToString(); - _portField.text = _portVal; + _portVal = MultiplayerManager.Instance.CurrentServer.Config.Port; + _portField.text = _portVal.ToString(); + new Thread(CheckPort).Start(); }; } + + private void CheckPort() + { + Singleton.instance.m_ThreadingWrapper.QueueMainThread(() => + { + _portState.text = "Checking port..."; + _portState.textColor = new Color32(255, 255, 0, 255); + _portState.tooltip = "Checking if port is reachable from the internet..."; + }); + + PortState state = IpAddress.CheckPort(_portVal); + Singleton.instance.m_ThreadingWrapper.QueueMainThread(() => + { + switch (state.status) + { + case HttpStatusCode.ServiceUnavailable: // Could not reach port + _portState.text = "Port is not reachable!"; + _portState.textColor = new Color32(255, 0, 0, 255); + _portState.tooltip = state.message; + break; + case HttpStatusCode.OK: // Success + _portState.text = "Port is reachable!"; + _portState.textColor = new Color32(0, 255, 0, 255); + _portState.tooltip = ""; + break; + default: // Something failed + _portState.text = "Failed to check port"; + _portState.textColor = new Color32(255, 0, 0, 255); + _portState.tooltip = state.message; + break; + } + }); + } } } diff --git a/src/csm/Util/CSMWebClient.cs b/src/csm/Util/CSMWebClient.cs new file mode 100644 index 00000000..90e24daa --- /dev/null +++ b/src/csm/Util/CSMWebClient.cs @@ -0,0 +1,53 @@ +using System; +using System.Net; + +namespace CSM.Util +{ + class CSMWebClient : WebClient + { + private WebRequest _request = null; + + protected override WebRequest GetWebRequest(Uri address) + { + this._request = base.GetWebRequest(address); + // Force IPv4 for now, TODO: Remove when CSM supports IPv6 + if (this._request is HttpWebRequest webRequest) + { + webRequest.ServicePoint.BindIPEndPointDelegate = (servicePoint, remoteEndPoint, retryCount) => + { + if (remoteEndPoint.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + return new IPEndPoint(IPAddress.Any, 0); + } + + throw new InvalidOperationException("no IPv4 address"); + }; + + webRequest.AllowAutoRedirect = false; + } + + return this._request; + } + + public HttpStatusCode StatusCode() + { + HttpStatusCode result; + + if (this._request == null) + { + throw (new InvalidOperationException("Unable to retrieve the status code, maybe you haven't made a request yet.")); + } + + if (base.GetWebResponse(this._request) is HttpWebResponse response) + { + result = response.StatusCode; + } + else + { + throw (new InvalidOperationException("Unable to retrieve the status code, maybe you haven't made a request yet.")); + } + + return result; + } + } +} From 95c4d63a0b82d36db01e20f1ab19dba1c92c1dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Borgsm=C3=BCller?= Date: Sun, 17 Jul 2022 21:35:08 +0200 Subject: [PATCH 2/4] Improve message, remove unused imports --- src/csm/Networking/IpAddress.cs | 1 - src/csm/Networking/Server.cs | 2 +- src/csm/Panels/ManageGamePanel.cs | 5 +---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/csm/Networking/IpAddress.cs b/src/csm/Networking/IpAddress.cs index bf69cfcf..efb19ca4 100644 --- a/src/csm/Networking/IpAddress.cs +++ b/src/csm/Networking/IpAddress.cs @@ -2,7 +2,6 @@ using System.IO; using System.Net; using System.Net.Sockets; -using System.ServiceModel.Channels; using System.Text; using CSM.API; using CSM.Util; diff --git a/src/csm/Networking/Server.cs b/src/csm/Networking/Server.cs index 25de531c..d63be4ab 100644 --- a/src/csm/Networking/Server.cs +++ b/src/csm/Networking/Server.cs @@ -130,7 +130,7 @@ private void CheckPort() if (automaticSuccess) { message = - "Port was forwarded automatically, but server is not reachable from the internet."; + "It was tried to forward the port automatically, but the server is not reachable from the internet. Manual port forwarding is required."; } else { diff --git a/src/csm/Panels/ManageGamePanel.cs b/src/csm/Panels/ManageGamePanel.cs index 4731ae80..2a6bcda3 100644 --- a/src/csm/Panels/ManageGamePanel.cs +++ b/src/csm/Panels/ManageGamePanel.cs @@ -1,12 +1,9 @@ -using System; -using System.Net; +using System.Net; using System.Threading; using ColossalFramework; using ColossalFramework.UI; -using CSM.API; using CSM.Helpers; using CSM.Networking; -using CSM.Util; using UnityEngine; namespace CSM.Panels From 264f15b3606dd0630bbd7b5374b969dfb116a792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Borgsm=C3=BCller?= Date: Sun, 24 Jul 2022 22:49:53 +0200 Subject: [PATCH 3/4] Add update checking mechanism --- src/csm/CSM.cs | 2 +- src/csm/Injections/MainMenuHandler.cs | 52 +++++++++++++++++++++++++-- src/csm/Panels/MessagePanel.cs | 49 ++++++++++++++++++++++++- src/csm/Panels/SettingsPanel.cs | 9 ++++- 4 files changed, 106 insertions(+), 6 deletions(-) diff --git a/src/csm/CSM.cs b/src/csm/CSM.cs index 9bc060c2..ae4c4bdb 100644 --- a/src/csm/CSM.cs +++ b/src/csm/CSM.cs @@ -43,7 +43,7 @@ public void OnEnabled() // Registers all other mods which implement the API ModSupport.Instance.Init(); // Setup join button - MainMenuHandler.CreateOrUpdateJoinGameButton(); + MainMenuHandler.Init(); Log.Info("Construction Complete!"); }); diff --git a/src/csm/Injections/MainMenuHandler.cs b/src/csm/Injections/MainMenuHandler.cs index 1bf7b342..7bc0e101 100644 --- a/src/csm/Injections/MainMenuHandler.cs +++ b/src/csm/Injections/MainMenuHandler.cs @@ -1,6 +1,11 @@ -using ColossalFramework.UI; +using System; +using System.Reflection; +using System.Threading; +using ColossalFramework.Threading; +using ColossalFramework.UI; using CSM.API; using CSM.Panels; +using CSM.Util; using HarmonyLib; using UnityEngine; @@ -16,13 +21,54 @@ public class MainMenuAwake /// public static void Prefix() { - MainMenuHandler.CreateOrUpdateJoinGameButton(); + MainMenuHandler.Init(); } } public static class MainMenuHandler { - public static void CreateOrUpdateJoinGameButton() + public static void Init() + { + CreateOrUpdateJoinGameButton(); + new Thread(() => CheckForUpdate(false)).Start(); + } + + public static void CheckForUpdate(bool alwaysShowInfo) + { + try + { + string latest = new CSMWebClient().DownloadString("http://csm-check.kaenganxt.dev/api/version"); + latest = latest.Substring(1); + string[] versionParts = latest.Split('.'); + Version latestVersion = new Version(int.Parse(versionParts[0]), int.Parse(versionParts[1])); + + Version version = Assembly.GetAssembly(typeof(CSM)).GetName().Version; + if (latestVersion > version) + { + Log.Info( + $"Update available! Current version: {version.Major}.{version.Minor} Latest version: {latestVersion.Major}.{latestVersion.Minor}"); + ThreadHelper.dispatcher.Dispatch(() => + { + MessagePanel panel = PanelManager.ShowPanel(); + panel.DisplayUpdateAvailable(version, latestVersion); + }); + } + else if (alwaysShowInfo) + { + ThreadHelper.dispatcher.Dispatch(() => + { + MessagePanel panel = PanelManager.ShowPanel(); + panel.DisplayNoUpdateAvailable(); + }); + } + } + catch (Exception e) + { + Log.Warn($"Failed to check for updates: {e.Message}"); + } + } + + private static void CreateOrUpdateJoinGameButton() { Log.Info("Creating join game button..."); diff --git a/src/csm/Panels/MessagePanel.cs b/src/csm/Panels/MessagePanel.cs index 61ba522e..c0bbbe2b 100644 --- a/src/csm/Panels/MessagePanel.cs +++ b/src/csm/Panels/MessagePanel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using ColossalFramework.UI; @@ -16,7 +17,8 @@ public class MessagePanel : UIPanel private string _title; private string _message; - private UIButton _closeButton; + private UIButton _closeButton, _githubButton; + private bool _githubShown = false; public override void Start() { @@ -47,6 +49,14 @@ public override void Start() this.AddScrollbar(messagePanel); + // Github button + _githubButton = this.CreateButton("Open GitHub", new Vector2(60, -340)); + _githubButton.eventClicked += (c, p) => + { + Process.Start("https://github.com/CitiesSkylinesMultiplayer/CSM/releases"); + }; + _githubButton.isVisible = _githubShown; + // Close button _closeButton = this.CreateButton("Close", new Vector2(60, -410)); @@ -72,6 +82,9 @@ private void SetMessage(string message) if (_messageLabel) _messageLabel.text = message; + + if (_githubButton) + _githubButton.Hide(); } public void DisplayContentWarning() @@ -173,5 +186,39 @@ public void DisplayReleaseNotes() Show(true); } + + public void DisplayUpdateAvailable(Version current, Version latest) + { + SetTitle("CSM Update available!"); + + string message = "A new update of the Cities: Skylines Multiplayer\n" + + "mod is available!\n\n" + + $"Current Version: {current.Major}.{current.Minor}\n" + + $"Latest Version: {latest.Major}.{latest.Minor}\n\n" + + "When using the Steam Workshop, it should be\n" + + "installed automatically (you may need to restart\n" + + "your game). Otherwise you can download the\n" + + "update from GitHub:\n\n" + + "https://github.com/CitiesSkylinesMultiplayer/\n" + + "CSM/releases"; + SetMessage(message); + + _githubShown = true; + if (_githubButton) + _githubButton.Show(); + + Show(true); + } + + public void DisplayNoUpdateAvailable() + { + SetTitle("CSM is up to date"); + + string message = "There is no update for the Cities: Skylines\n" + + "Multiplayer mod available."; + SetMessage(message); + + Show(true); + } } } diff --git a/src/csm/Panels/SettingsPanel.cs b/src/csm/Panels/SettingsPanel.cs index af938c67..aa4b96fe 100644 --- a/src/csm/Panels/SettingsPanel.cs +++ b/src/csm/Panels/SettingsPanel.cs @@ -1,5 +1,7 @@ -using ColossalFramework.UI; +using System.Threading; +using ColossalFramework.UI; using CSM.API; +using CSM.Injections; using CSM.Mods; using ICities; @@ -38,6 +40,11 @@ public static void Build(UIHelperBase helper, Settings settings) MessagePanel panel = PanelManager.ShowPanel(); panel.DisplayReleaseNotes(); }); + + advancedGroup.AddButton("Check for updates", () => + { + new Thread(() => MainMenuHandler.CheckForUpdate(true)).Start(); + }); } } } From 6979bf1e1ef77423352be1feb0e31c8b82572d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Borgsm=C3=BCller?= Date: Sat, 20 Aug 2022 17:56:18 +0200 Subject: [PATCH 4/4] Use API domain --- src/csm/Injections/MainMenuHandler.cs | 2 +- src/csm/Networking/IpAddress.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/csm/Injections/MainMenuHandler.cs b/src/csm/Injections/MainMenuHandler.cs index 7bc0e101..e0e51e7b 100644 --- a/src/csm/Injections/MainMenuHandler.cs +++ b/src/csm/Injections/MainMenuHandler.cs @@ -37,7 +37,7 @@ public static void CheckForUpdate(bool alwaysShowInfo) { try { - string latest = new CSMWebClient().DownloadString("http://csm-check.kaenganxt.dev/api/version"); + string latest = new CSMWebClient().DownloadString("http://api.citiesskylinesmultiplayer.com/api/version"); latest = latest.Substring(1); string[] versionParts = latest.Split('.'); Version latestVersion = new Version(int.Parse(versionParts[0]), int.Parse(versionParts[1])); diff --git a/src/csm/Networking/IpAddress.cs b/src/csm/Networking/IpAddress.cs index efb19ca4..6fbb7044 100644 --- a/src/csm/Networking/IpAddress.cs +++ b/src/csm/Networking/IpAddress.cs @@ -37,8 +37,8 @@ public static string GetLocalIpAddress() //Create a new socket using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0)) { - //Connect to 8.8.8.8 (Google IP) - socket.Connect("csm-check.kaenganxt.dev", 65530); + //Connect to some server to non-listening port + socket.Connect("api.citiesskylinesmultiplayer.com", 65530); //Get the IPEndPoint IPEndPoint endPoint = socket.LocalEndPoint as IPEndPoint; //Get the IP Address (Internal) from the IPEndPoint @@ -63,7 +63,7 @@ public static string GetExternalIpAddress() try { //Get the External IP address from internet - _externalIp = new CSMWebClient().DownloadString("http://csm-check.kaenganxt.dev/api/ip"); + _externalIp = new CSMWebClient().DownloadString("http://api.citiesskylinesmultiplayer.com/api/ip"); return _externalIp; } catch (Exception e) @@ -79,7 +79,7 @@ public static PortState CheckPort(int port) CSMWebClient client = new CSMWebClient(); try { - string answer = client.DownloadString("http://csm-check.kaenganxt.dev/api/check?port=" + port); + string answer = client.DownloadString("http://api.citiesskylinesmultiplayer.com/api/check?port=" + port); return new PortState(answer, client.StatusCode()); } catch (WebException e)