├── .gitignore ├── LICENSE ├── Monitor ├── DatabaseRecord.cs ├── Monitor.cs └── SteamManager.cs ├── Program.cs ├── README.md ├── SteamMonitor.csproj ├── SteamMonitor.sln ├── Utils ├── Log.cs └── SteamMonitorUser.cs └── schema.sql /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 2 | [Bb]in/ 3 | [Oo]bj/ 4 | 5 | # mstest test results 6 | TestResults 7 | 8 | ## Ignore Visual Studio temporary files, build results, and 9 | ## files generated by popular Visual Studio add-ons. 10 | 11 | # User-specific files 12 | *.suo 13 | *.user 14 | *.userprefs 15 | *.sln.docstates 16 | 17 | # Build results 18 | [Dd]ebug/ 19 | [Rr]elease/ 20 | x64/ 21 | *_i.c 22 | *_p.c 23 | *.ilk 24 | *.meta 25 | *.obj 26 | *.pch 27 | *.pdb 28 | *.pgc 29 | *.pgd 30 | *.rsp 31 | *.sbr 32 | *.tlb 33 | *.tli 34 | *.tlh 35 | *.tmp 36 | *.log 37 | *.vspscc 38 | *.vssscc 39 | .builds 40 | 41 | # Visual C++ cache files 42 | ipch/ 43 | *.aps 44 | *.ncb 45 | *.opensdf 46 | *.sdf 47 | 48 | # Visual Studio profiler 49 | *.psess 50 | *.vsp 51 | *.vspx 52 | 53 | # Guidance Automation Toolkit 54 | *.gpState 55 | 56 | # ReSharper is a .NET coding add-in 57 | _ReSharper* 58 | 59 | # NCrunch 60 | *.ncrunch* 61 | .*crunch*.local.xml 62 | 63 | # Installshield output folder 64 | [Ee]xpress 65 | 66 | # DocProject is a documentation generator add-in 67 | DocProject/buildhelp/ 68 | DocProject/Help/*.HxT 69 | DocProject/Help/*.HxC 70 | DocProject/Help/*.hhc 71 | DocProject/Help/*.hhk 72 | DocProject/Help/*.hhp 73 | DocProject/Help/Html2 74 | DocProject/Help/html 75 | 76 | # Click-Once directory 77 | publish 78 | 79 | # Publish Web Output 80 | *.Publish.xml 81 | 82 | # NuGet Packages Directory 83 | packages 84 | *.nupkg 85 | 86 | # Windows Azure Build Output 87 | csx 88 | *.build.csdef 89 | 90 | # Windows Store app package directory 91 | AppPackages/ 92 | 93 | # Others 94 | [Bb]in 95 | [Oo]bj 96 | sql 97 | TestResults 98 | [Tt]est[Rr]esult* 99 | *.Cache 100 | ClientBin 101 | [Ss]tyle[Cc]op.* 102 | ~$* 103 | *.dbmdl 104 | Generated_Code #added for RIA/Silverlight projects 105 | 106 | # Backup & report files from converting an old project file to a newer 107 | # Visual Studio version. Backup files are not needed, because we have git ;-) 108 | _UpgradeReport_Files/ 109 | Backup*/ 110 | UpgradeLog*.XML 111 | .vs/ 112 | 113 | # third party libs 114 | boost/ 115 | google/ 116 | zlib/ 117 | protobuf/ 118 | cryptopp/ 119 | 120 | # misc 121 | Thumbs.db 122 | .DS_Store 123 | 124 | # asp/web 125 | PublishProfiles -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ryan Stecker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Monitor/DatabaseRecord.cs: -------------------------------------------------------------------------------- 1 | using SteamKit2; 2 | using SteamKit2.Discovery; 3 | 4 | namespace StatusService 5 | { 6 | class DatabaseRecord 7 | { 8 | public string Hostname { get; } 9 | public int Port { get; } 10 | public bool IsWebSocket { get; } 11 | public string Datacenter { get; } 12 | 13 | public DatabaseRecord(string address, string datacenter, bool isWebsocket) 14 | { 15 | var indexOfColon = address.IndexOf(':'); 16 | var portNumber = address[(indexOfColon + 1)..]; 17 | 18 | Hostname = address[..indexOfColon]; 19 | Port = int.Parse(portNumber); 20 | IsWebSocket = isWebsocket; 21 | Datacenter = datacenter; 22 | } 23 | 24 | public ServerRecord GetServerRecord() => 25 | ServerRecord.CreateServer(Hostname, Port, IsWebSocket ? ProtocolTypes.WebSocket : ProtocolTypes.Tcp); 26 | 27 | public string GetUniqueKey() => $"{IsWebSocket}@{Hostname}"; 28 | public string GetString() => $"{Hostname}:{Port}"; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Monitor/Monitor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using SteamKit2; 5 | 6 | namespace StatusService 7 | { 8 | class Monitor 9 | { 10 | public DatabaseRecord Server { get; set; } 11 | public uint Reconnecting { get; set; } 12 | 13 | readonly SteamClient Client; 14 | readonly SteamMonitorUser steamUser; 15 | readonly CallbackManager callbackMgr; 16 | readonly CancellationToken cancellationToken; 17 | 18 | bool IsDisconnecting; 19 | 20 | public EResult LastReportedStatus { get; set; } 21 | public DateTime LastSeen { get; set; } 22 | DateTime LastSuccess = DateTime.Now; 23 | DateTime nextConnect = DateTime.MaxValue; 24 | 25 | private static readonly TimeSpan NoSuccessRemoval = TimeSpan.FromDays(1); 26 | 27 | public Monitor(DatabaseRecord server, SteamConfiguration config) 28 | { 29 | Server = server; 30 | 31 | Client = new SteamClient(config); 32 | 33 | steamUser = new SteamMonitorUser(); 34 | Client.AddHandler(steamUser); 35 | 36 | callbackMgr = new CallbackManager(Client); 37 | callbackMgr.Subscribe(OnConnected); 38 | callbackMgr.Subscribe(OnDisconnected); 39 | callbackMgr.Subscribe(OnLoggedOn); 40 | callbackMgr.Subscribe(OnLoggedOff); 41 | 42 | cancellationToken = Program.Cts.Token; 43 | 44 | Task.Factory.StartNew(HandleCallbacks, cancellationToken, TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness, TaskScheduler.Default); 45 | } 46 | 47 | public void Connect(DateTime when) 48 | { 49 | nextConnect = when; 50 | } 51 | 52 | public void Disconnect() 53 | { 54 | IsDisconnecting = true; 55 | Client.Disconnect(); 56 | } 57 | 58 | public async Task HandleCallbacks() 59 | { 60 | try 61 | { 62 | while (!cancellationToken.IsCancellationRequested) 63 | { 64 | await callbackMgr.RunWaitCallbackAsync(cancellationToken); 65 | } 66 | } 67 | catch (OperationCanceledException) 68 | { 69 | // 70 | } 71 | } 72 | 73 | public void DoTick(DateTime now) 74 | { 75 | if (now >= nextConnect) 76 | { 77 | nextConnect = now + TimeSpan.FromMinutes(1); 78 | 79 | Reconnecting++; 80 | 81 | Task.Run(() => Client.Connect(Server.GetServerRecord())); 82 | } 83 | } 84 | 85 | private void OnConnected(SteamClient.ConnectedCallback callback) 86 | { 87 | Reconnecting = 0; 88 | 89 | steamUser.LogOn(); 90 | } 91 | 92 | private void OnDisconnected(SteamClient.DisconnectedCallback callback) 93 | { 94 | if (IsDisconnecting) 95 | { 96 | return; 97 | } 98 | 99 | var now = DateTime.Now; 100 | 101 | if (LastSuccess + NoSuccessRemoval < now && LastSeen + NoSuccessRemoval < now) 102 | { 103 | IsDisconnecting = true; 104 | Task.Run(() => SteamManager.Instance.RemoveCM(this)); 105 | return; 106 | } 107 | 108 | var numSeconds = Random.Shared.Next(10, 60); 109 | Connect(now + TimeSpan.FromSeconds(numSeconds)); 110 | 111 | // If Steam dies, don't say next connect is planned 112 | if (Reconnecting == 0) 113 | { 114 | Reconnecting = 2; 115 | } 116 | 117 | if (Reconnecting >= 10) 118 | { 119 | SteamManager.Instance.NotifyCMOffline(this, EResult.NoConnection, $"Disconnected (#{Reconnecting}) (Seen: {LastSeen} Success: {LastSuccess})"); 120 | } 121 | else if (Reconnecting == 1) 122 | { 123 | SteamManager.Instance.NotifyCMOffline(this, EResult.OK, $"Reconnecting"); 124 | } 125 | else 126 | { 127 | SteamManager.Instance.NotifyCMOffline(this, EResult.NoConnection, $"Disconnected (#{Reconnecting})"); 128 | } 129 | } 130 | 131 | private void OnLoggedOn(SteamUser.LoggedOnCallback callback) 132 | { 133 | if (callback.Result != EResult.OK) 134 | { 135 | SteamManager.Instance.NotifyCMOffline(this, callback.Result, "Logon error"); 136 | 137 | return; 138 | } 139 | 140 | SteamManager.Instance.NotifyCMOnline(this); 141 | 142 | LastSuccess = DateTime.Now; 143 | 144 | // schedule a random reconnect 145 | Connect(LastSuccess 146 | + TimeSpan.FromMinutes(5) 147 | + TimeSpan.FromMinutes(Random.Shared.NextDouble() * 5)); 148 | } 149 | 150 | private void OnLoggedOff(SteamUser.LoggedOffCallback callback) 151 | { 152 | SteamManager.Instance.NotifyCMOffline(this, callback.Result, "Logged off"); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Monitor/SteamManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net.Http; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using MySqlConnector; 10 | using SteamKit2; 11 | 12 | namespace StatusService 13 | { 14 | class SteamManager 15 | { 16 | private const int HighestCellId = 220; 17 | 18 | public static SteamManager Instance { get; } = new(); 19 | 20 | readonly ConcurrentDictionary monitors; 21 | readonly SteamConfiguration SharedConfig; 22 | readonly string databaseConnectionString; 23 | DateTime NextCMListUpdate; 24 | uint CellID; 25 | 26 | private SteamManager() 27 | { 28 | monitors = new ConcurrentDictionary(); 29 | 30 | SharedConfig = SteamConfiguration.Create(b => b 31 | .WithDirectoryFetch(false) 32 | .WithProtocolTypes(ProtocolTypes.WebSocket) 33 | .WithConnectionTimeout(TimeSpan.FromSeconds(15)) 34 | ); 35 | 36 | var path = Path.Combine(AppContext.BaseDirectory, "database.txt"); 37 | 38 | if (!File.Exists(path)) 39 | { 40 | Log.WriteError("Put your MySQL connection string in database.txt"); 41 | 42 | Environment.Exit(1); 43 | } 44 | 45 | databaseConnectionString = File.ReadAllText(path).Trim(); 46 | } 47 | 48 | public async Task Start() 49 | { 50 | NextCMListUpdate = DateTime.Now.AddMinutes(20); 51 | 52 | await using var db = await GetConnection(); 53 | var servers = new List(); 54 | 55 | // Seed CM list with old CMs in the database 56 | await using var cmd = new MySqlCommand("SELECT `Address`, `IsWebSocket`, `Datacenter` FROM `CMs`", db); 57 | await using var reader = await cmd.ExecuteReaderAsync(); 58 | while (await reader.ReadAsync()) 59 | { 60 | var address = reader.GetString(0); 61 | var isWebSocket = reader.GetBoolean(1); 62 | var datacenter = reader.GetString(2); 63 | 64 | servers.Add(new DatabaseRecord(address, datacenter, isWebSocket)); 65 | } 66 | 67 | Log.WriteInfo($"Got {servers.Count} old CMs"); 68 | 69 | await UpdateCMList(servers); 70 | 71 | _ = Task.Run(ScanAllCellIds); 72 | } 73 | 74 | public async Task Stop() 75 | { 76 | foreach (var monitor in monitors.Values) 77 | { 78 | Log.WriteInfo($"Disconnecting monitor {monitor.Server.GetString()}"); 79 | 80 | monitor.Disconnect(); 81 | } 82 | 83 | Log.WriteInfo("All monitors disconnected"); 84 | 85 | // Reset all statuses 86 | await using var db = await GetConnection(); 87 | await using var cmd = new MySqlCommand($"UPDATE `CMs` SET `Status` = {(int)EResult.Invalid}", db); 88 | await cmd.ExecuteNonQueryAsync(); 89 | } 90 | 91 | public void Tick() 92 | { 93 | var monitorsCached = monitors.Values.ToList(); 94 | var now = DateTime.Now; 95 | 96 | foreach (var monitor in monitorsCached) 97 | { 98 | monitor.DoTick(now); 99 | } 100 | 101 | if (now > NextCMListUpdate) 102 | { 103 | NextCMListUpdate = now + TimeSpan.FromMinutes(11) + TimeSpan.FromSeconds(Random.Shared.Next(10, 120)); 104 | 105 | Task.Run(UpdateCMListViaWebAPI); 106 | } 107 | 108 | Thread.Sleep(1000); 109 | } 110 | 111 | public async Task RemoveCM(Monitor monitor) 112 | { 113 | var address = monitor.Server.GetString(); 114 | 115 | Log.WriteInfo($"Removing server: {address}"); 116 | 117 | monitors.TryRemove(monitor.Server.GetUniqueKey(), out _); 118 | 119 | try 120 | { 121 | await using var db = await GetConnection(); 122 | await using var cmd = new MySqlCommand("DELETE FROM `CMs` WHERE `Address` = @Address AND `IsWebSocket` = @IsWebSocket", db); 123 | cmd.Parameters.AddWithValue("@Address", address); 124 | cmd.Parameters.AddWithValue("@IsWebSocket", monitor.Server.IsWebSocket); 125 | 126 | await cmd.ExecuteNonQueryAsync(); 127 | } 128 | catch (MySqlException e) 129 | { 130 | Log.WriteError($"Failed to remove server: {e.Message}"); 131 | } 132 | } 133 | 134 | private async Task UpdateCMList(IEnumerable cmList) 135 | { 136 | var x = 0; 137 | var now = DateTime.Now; 138 | var changed = new HashSet(); 139 | 140 | foreach (var cm in cmList.OrderBy(cm => cm.Port)) 141 | { 142 | var uniqueKey = cm.GetUniqueKey(); 143 | 144 | if (monitors.TryGetValue(uniqueKey, out var monitor)) 145 | { 146 | monitor.LastSeen = now; 147 | 148 | // Server on a particular port may be dead, so change it 149 | // for tcp servers port 27017 to be definitive 150 | // for websockets, there's not always 443 port, and other ports follow tcp ones 151 | if (monitor.Reconnecting > 2 && monitor.Server.Port != cm.Port && !changed.Contains(uniqueKey)) 152 | { 153 | Log.WriteInfo($"Changed {monitor.Server.GetString()} to {cm.GetString()}"); 154 | 155 | try 156 | { 157 | await using var db = await GetConnection(); 158 | await using var cmd = new MySqlCommand("UPDATE `CMs` SET `Address` = @Address WHERE `Address` = @OldAddress AND `IsWebSocket` = @IsWebSocket", db); 159 | cmd.Parameters.AddWithValue("@Address", cm.GetString()); 160 | cmd.Parameters.AddWithValue("@OldAddress", monitor.Server.GetString()); 161 | cmd.Parameters.AddWithValue("@IsWebSocket", monitor.Server.IsWebSocket); 162 | 163 | await cmd.ExecuteNonQueryAsync(); 164 | } 165 | catch (MySqlException e) 166 | { 167 | Log.WriteError($"Failed to change {monitor.Server.GetString()}: {e.Message}"); 168 | } 169 | 170 | monitor.Server = cm; 171 | changed.Add(uniqueKey); 172 | } 173 | 174 | continue; 175 | } 176 | 177 | var newMonitor = new Monitor(cm, SharedConfig); 178 | 179 | monitors.TryAdd(uniqueKey, newMonitor); 180 | 181 | await UpdateCMStatus(newMonitor, EResult.Pending, "New server"); 182 | 183 | newMonitor.Connect(now + TimeSpan.FromSeconds(++x % 40)); 184 | } 185 | 186 | if (x > 0) 187 | { 188 | Log.WriteInfo($"There are now {monitors.Count} monitors, added {x} new ones"); 189 | } 190 | } 191 | 192 | public void NotifyCMOnline(Monitor monitor) 193 | { 194 | _ = UpdateCMStatus(monitor, EResult.OK, "Online"); 195 | } 196 | 197 | public void NotifyCMOffline(Monitor monitor, EResult result, string lastAction) 198 | { 199 | _ = UpdateCMStatus(monitor, result, lastAction); 200 | } 201 | 202 | private async Task UpdateCMStatus(Monitor monitor, EResult result, string lastAction) 203 | { 204 | var keyName = monitor.Server.GetString(); 205 | var type = monitor.Server.IsWebSocket ? "WS" : "TCP"; 206 | 207 | Log.WriteStatus($"> {keyName,40} | {type,-3} | {monitor.Server.Datacenter,-4} | {result,-20} | {lastAction}"); 208 | 209 | if (monitor.LastReportedStatus == result) 210 | { 211 | return; 212 | } 213 | 214 | try 215 | { 216 | await using var db = await GetConnection(); 217 | await using var cmd = new MySqlCommand("INSERT INTO `CMs` (`Address`, `IsWebSocket`, `Datacenter`, `Status`) VALUES(@IP, @IsWebSocket, @Datacenter, @Status) ON DUPLICATE KEY UPDATE `Status` = VALUES(`Status`)", db); 218 | cmd.Parameters.AddWithValue("@IP", keyName); 219 | cmd.Parameters.AddWithValue("@IsWebSocket", monitor.Server.IsWebSocket); 220 | cmd.Parameters.AddWithValue("@Datacenter", monitor.Server.Datacenter); 221 | cmd.Parameters.AddWithValue("@Status", (int)result); 222 | 223 | await cmd.ExecuteNonQueryAsync(); 224 | 225 | monitor.LastReportedStatus = result; 226 | } 227 | catch (MySqlException e) 228 | { 229 | Log.WriteError($"Failed to update status of {keyName}: {e.Message}"); 230 | } 231 | } 232 | 233 | private async Task UpdateCMListViaWebAPI() 234 | { 235 | Log.WriteInfo("Updating CM list using webapi"); 236 | 237 | try 238 | { 239 | var globalServers = (await LoadCMList(SharedConfig, CellID)).ToList(); 240 | 241 | Log.WriteInfo($"Got {globalServers.Count} servers from cell {CellID}"); 242 | 243 | if (CellID % 10 == 0) 244 | { 245 | var chinaRealmServers = (await LoadCMList(SharedConfig, 47)).Where(s => !globalServers.Contains(s)).ToList(); // Shanghai cell 246 | var servers = globalServers.Concat(chinaRealmServers); 247 | 248 | Log.WriteInfo($"Got {chinaRealmServers.Count} chinese servers"); 249 | 250 | await UpdateCMList(servers); 251 | } 252 | else 253 | { 254 | await UpdateCMList(globalServers); 255 | } 256 | } 257 | catch (Exception e) 258 | { 259 | Log.WriteError($"Web API Exception: {e}"); 260 | } 261 | 262 | if (++CellID >= HighestCellId) 263 | { 264 | CellID = 0; 265 | } 266 | } 267 | 268 | private async Task ScanAllCellIds() 269 | { 270 | Log.WriteInfo("Updating CM list using webapi by checking all cellids"); 271 | 272 | foreach (var cellId in Enumerable.Range(0, HighestCellId).OrderBy(x => Random.Shared.Next()).Select(v => (uint)v)) 273 | { 274 | try 275 | { 276 | var servers = (await LoadCMList(SharedConfig, cellId)).ToList(); 277 | 278 | Log.WriteInfo($"Got {servers.Count} servers from cell {cellId}"); 279 | 280 | await UpdateCMList(servers); 281 | await Task.Delay(Random.Shared.Next(10, 1000)); 282 | } 283 | catch (Exception e) 284 | { 285 | Log.WriteError($"Web API Exception: {e}"); 286 | } 287 | } 288 | } 289 | 290 | private async Task GetConnection() 291 | { 292 | var connection = new MySqlConnection(databaseConnectionString); 293 | 294 | await connection.OpenAsync(Program.Cts.Token); 295 | 296 | return connection; 297 | } 298 | 299 | private static async Task> LoadCMList(SteamConfiguration configuration, uint cellId) 300 | { 301 | var directory = configuration.GetAsyncWebAPIInterface("ISteamDirectory"); 302 | var args = new Dictionary 303 | { 304 | ["cmtype"] = "websockets", 305 | ["cellid"] = cellId.ToString(), 306 | ["maxcount"] = int.MaxValue.ToString(), 307 | }; 308 | 309 | if (cellId == 47) 310 | { 311 | args.Add("realm", "steamchina"); 312 | } 313 | 314 | var response = await directory.CallAsync(HttpMethod.Get, "GetCMListForConnect", 1, args); 315 | var serverList = response["serverlist"]; 316 | var serverRecords = new List(serverList.Children.Count); 317 | 318 | foreach (var child in serverList.Children) 319 | { 320 | var endpoint = child["endpoint"].AsString(); 321 | var dc = child["dc"].AsString(); 322 | 323 | if (endpoint == null || dc == null) 324 | { 325 | continue; 326 | } 327 | 328 | serverRecords.Add(new DatabaseRecord( 329 | endpoint, 330 | dc, 331 | child["type"].AsString() == "websockets" 332 | )); 333 | } 334 | 335 | return serverRecords; 336 | } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace StatusService 6 | { 7 | static class Program 8 | { 9 | public static CancellationTokenSource Cts = new(); 10 | 11 | static async Task Main() 12 | { 13 | Console.Title = "Steam Monitor"; 14 | 15 | Console.CancelKeyPress += delegate 16 | { 17 | Log.WriteInfo("Stopping via Ctrl-C..."); 18 | 19 | Cts.Cancel(); 20 | 21 | Environment.Exit(0); 22 | }; 23 | 24 | AppDomain.CurrentDomain.ProcessExit += (sender, e) => 25 | { 26 | Cts.Cancel(); 27 | }; 28 | 29 | AppDomain.CurrentDomain.UnhandledException += (sender, e) => 30 | { 31 | Log.WriteError($"Unhandled exception: {e.ExceptionObject}"); 32 | }; 33 | 34 | await SteamManager.Instance.Start(); 35 | 36 | try 37 | { 38 | using var timer = new PeriodicTimer(TimeSpan.FromSeconds(4)); 39 | var token = Cts.Token; 40 | 41 | while (await timer.WaitForNextTickAsync(token)) 42 | { 43 | SteamManager.Instance.Tick(); 44 | } 45 | } 46 | catch (OperationCanceledException) 47 | { 48 | // 49 | } 50 | 51 | Log.WriteInfo("Stopping..."); 52 | await SteamManager.Instance.Stop(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | steamstatus 2 | =========== 3 | 4 | A quick and dirty POC website to view the status of Steam CM servers. 5 | -------------------------------------------------------------------------------- /SteamMonitor.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net9.0 4 | LatestMajor 5 | Exe 6 | embedded 7 | true 8 | true 9 | false 10 | false 11 | enable 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /SteamMonitor.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31815.197 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SteamMonitor", "SteamMonitor.csproj", "{16A036E0-6EEE-4EE7-BB3E-B532EEF96BF5}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {16A036E0-6EEE-4EE7-BB3E-B532EEF96BF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {16A036E0-6EEE-4EE7-BB3E-B532EEF96BF5}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {16A036E0-6EEE-4EE7-BB3E-B532EEF96BF5}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {16A036E0-6EEE-4EE7-BB3E-B532EEF96BF5}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {40139F56-B3A0-4BFD-A074-AA69ADA4A856} 24 | EndGlobalSection 25 | GlobalSection(MonoDevelopProperties) = preSolution 26 | StartupItem = StatusService.csproj 27 | Policies = $0 28 | $0.DotNetNamingPolicy = $1 29 | $1.DirectoryNamespaceAssociation = None 30 | $1.ResourceNamePolicy = FileFormatDefault 31 | $0.TextStylePolicy = $7 32 | $2.inheritsSet = VisualStudio 33 | $2.inheritsScope = text/plain 34 | $2.scope = text/x-csharp 35 | $0.CSharpFormattingPolicy = $3 36 | $3.IndentSwitchBody = True 37 | $3.IndentBlocksInsideExpressions = True 38 | $3.AnonymousMethodBraceStyle = NextLine 39 | $3.PropertyBraceStyle = NextLine 40 | $3.PropertyGetBraceStyle = NextLine 41 | $3.PropertySetBraceStyle = NextLine 42 | $3.EventBraceStyle = NextLine 43 | $3.EventAddBraceStyle = NextLine 44 | $3.EventRemoveBraceStyle = NextLine 45 | $3.StatementBraceStyle = NextLine 46 | $3.ElseNewLinePlacement = NewLine 47 | $3.CatchNewLinePlacement = NewLine 48 | $3.FinallyNewLinePlacement = NewLine 49 | $3.WhileNewLinePlacement = DoNotCare 50 | $3.ArrayInitializerWrapping = DoNotChange 51 | $3.ArrayInitializerBraceStyle = NextLine 52 | $3.BeforeMethodDeclarationParentheses = False 53 | $3.BeforeMethodCallParentheses = False 54 | $3.BeforeConstructorDeclarationParentheses = False 55 | $3.NewLineBeforeConstructorInitializerColon = NewLine 56 | $3.NewLineAfterConstructorInitializerColon = SameLine 57 | $3.BeforeDelegateDeclarationParentheses = False 58 | $3.NewParentheses = False 59 | $3.SpacesBeforeBrackets = False 60 | $3.inheritsSet = Mono 61 | $3.inheritsScope = text/x-csharp 62 | $3.scope = text/x-csharp 63 | $4.inheritsSet = VisualStudio 64 | $4.inheritsScope = text/plain 65 | $4.scope = text/plain 66 | $5.inheritsSet = null 67 | $5.scope = application/config+xml 68 | $0.XmlFormattingPolicy = $8 69 | $6.inheritsSet = null 70 | $6.scope = application/config+xml 71 | $7.inheritsSet = null 72 | $7.scope = application/xml 73 | $8.inheritsSet = Mono 74 | $8.inheritsScope = application/xml 75 | $8.scope = application/xml 76 | EndGlobalSection 77 | EndGlobal 78 | -------------------------------------------------------------------------------- /Utils/Log.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StatusService 4 | { 5 | public static class Log 6 | { 7 | public static void WriteStatus(string log) 8 | { 9 | WriteLine(log); 10 | } 11 | 12 | public static void WriteInfo(string log) 13 | { 14 | Console.ForegroundColor = ConsoleColor.Green; 15 | WriteLine(log); 16 | Console.ResetColor(); 17 | } 18 | 19 | public static void WriteError(string log) 20 | { 21 | Console.ForegroundColor = ConsoleColor.Red; 22 | WriteLine(log); 23 | Console.ResetColor(); 24 | } 25 | 26 | private static void WriteLine(string log) 27 | { 28 | Console.WriteLine($"{DateTime.Now:HH:mm:ss} {log}"); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Utils/SteamMonitorUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SteamKit2; 3 | using SteamKit2.Internal; 4 | 5 | namespace StatusService 6 | { 7 | class SteamMonitorUser : ClientMsgHandler 8 | { 9 | public void LogOn() 10 | { 11 | var logonMsg = new ClientMsgProtobuf(EMsg.ClientLogon); 12 | 13 | var steamId = new SteamID(0, SteamID.AllInstances, Client.Universe, EAccountType.AnonUser); 14 | var randomIp = (uint)Random.Shared.Next(0, int.MaxValue); 15 | 16 | logonMsg.ProtoHeader.steamid = steamId; 17 | logonMsg.Body.protocol_version = MsgClientLogon.CurrentProtocol; 18 | logonMsg.Body.obfuscated_private_ip = new CMsgIPAddress { v4 = randomIp }; 19 | logonMsg.Body.deprecated_obfustucated_private_ip = randomIp; 20 | 21 | Client.Send(logonMsg); 22 | 23 | // See https://github.com/SteamRE/SteamKit/blob/f4ff8ed85155a9868c4ae730298847b4957b7a5d/SteamKit2/SteamKit2/Steam/Handlers/SteamGameServer/SteamGameServer.cs#L134 24 | } 25 | 26 | public override void HandleMsg(IPacketMsg packetMsg) 27 | { 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `CMs` ( 2 | `Address` varchar(50) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, 3 | `Datacenter` varchar(5) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, 4 | `IsWebSocket` tinyint(1) NOT NULL DEFAULT 0, 5 | `Status` smallint(6) NOT NULL, 6 | `LastUpdate` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), 7 | PRIMARY KEY (`Address`,`IsWebSocket`), 8 | KEY `Status` (`Status`) 9 | ) ENGINE=InnoDB DEFAULT CHARSET=ascii COLLATE=ascii_bin; 10 | --------------------------------------------------------------------------------