-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathGenericConsole.hpp
More file actions
437 lines (380 loc) · 15.5 KB
/
GenericConsole.hpp
File metadata and controls
437 lines (380 loc) · 15.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
/* vim: set noet ts=4 sw=4 sts=4 ft=cpp:
*
* Created by Darkwire Software.
*
* This example server file is available unlicensed; the MIT license of liblacewing/Lacewing Relay does not apply to this file.
*
* This file is used as a template to make OS-specific code. Several preprocessor-level things are both handled here, and
* have their handling made redundant by the pre-build script.
*
* You should not modify this template unless you are writing code for all platforms.
*/
#pragma once
// Console imports
#include <iostream>
#include <sstream>
#include <fstream>
#include <algorithm>
#include <vector>
#if __has_include(<filesystem>)
#include <filesystem>
#endif
#include <functional> // used for console's code for error reporting
#include <cmath> // used for std::ceil
#include <iomanip>
// Windows support of UTF-8 console is limited, particularly below Win 10.
// UTF-16 WinAPI console is also somewhat iffy, producing questionable results from std::wcout if console is
// redirected to file, or console buffering makes half a Unicode code point.
// Use of UTF-8 with WinAPI A text, ANSI virtual terminal commands, and Windows Terminal, were all introduced
// at around the same time, in later builds of Windows 10, probably in line with release of WSL.
// For more reading on Unicode terms, see https://dark-wire.com/relay/help/Unicodenotes.html .
//
// Using UTF-8 doesn't work consistently either as it also may split.
//
// However, if you want to try using only UTF-8 and POSIX, you can do so.
// These are used to prepend string literals.
#if (defined(__cpp_lib_char8_t) && __cpp_lib_char8_t >= 202207L)
# define TXT(x) u8##x
# define lw_utf8_console 1
# define u8_lw(x) std::u8string_view(x)
# define lw_u8(x) x
using lw_char = std::char_8t;
#elif defined(_WIN32) && defined(_UNICODE)
using lw_char = wchar_t;
# define TXT(x) L##x
# define u8_lw(x) UTF8ToWide(x)
# define lw_u8(x) WideToUTF8(x)
#else
# define TXT(x) x
# define lw_utf8_console 1
# ifndef _WIN32
# define lw_utf8_console 1
# endif
# define u8_lw(x) x
# define lw_u8(x) x
using lw_char = char;
#endif
using lw_string = std::basic_string<lw_char>;
using lw_string_view = std::basic_string_view<lw_char>;
using lw_stringstream = std::basic_stringstream<lw_char>;
using lw_fstream = std::basic_fstream<lw_char>;
#include "Lacewing/Lacewing.h"
using namespace std::string_view_literals;
using namespace std::string_literals;
using namespace std::chrono_literals;
#include <locale>
// Contact message for TCP upload or unrecognised messages are sent, and user gets themselves kicked or banned.
// When set, must be prefaced with a space, as it is appended to ban/kick messages.
std::string contactMsg;
// Upload limit for ENTIRE SERVER, TCP + UDP, in bytes. 0 is unlimited.
// UDP messages received above 4/5ths of this limit will be discarded, so TCP has room.
// TCP messages received above this limit are still delivered.
std::size_t totalUploadCap = 0;
// TCP upload limit for single clients, per second, in bytes. 0 is unlimited.
// TCP messages received above this limit will send the client an error message
// and disconnect them.
// UDP upload limit is not defined.
std::size_t tcpClientUploadCap = 0;
// Main raw socket, serving Windows, Android, iOS, Mac, and others that use plain TCP/UDP sockets,
// as opposed to WebSocket.
lw_ui16 mainPort = 0;
// If Flash policy server is hosting. Policy file can be generated by this app, or loaded from path.
bool flashEnabled = false;
std::string flashPolicyPath;
bool deleteFlashPolicyAtEndOfApp = false;
// Set to non-zero by presence of certs, or by commandline arg.
// websocketSecure will not work without certificate loading before websocket host is called.
// WebSocket expects ./fullchain.pem and ./privkey.pem files, with no password, in same folder as executable.
// Windows can also use ./tlscert.pfx, make sure private key is inside, with no password.
lw_ui16 websocketNonSecurePort = 0, websocketSecurePort = 0;
std::string wsFullChainPath, wsPrivKeyPath, wsPassPhrase;
std::string welcomeMessage;
// Holds current time and a separator
lw_char timeBuffer[sizeof("HH:MM:SS | ")] = {};
// Declarations - Lacewing handlers
void OnConnectRequest(lacewing::relayserver& server, std::shared_ptr<lacewing::relayserver::client> client);
void OnDisconnect(lacewing::relayserver& server, std::shared_ptr<lacewing::relayserver::client> client);
void OnError(lacewing::relayserver& server, lacewing::error error);
void OnServerMessage(lacewing::relayserver& server, std::shared_ptr<lacewing::relayserver::client> senderclient,
bool blasted, lw_ui8 subchannel, std::string_view data, lw_ui8 variant);
void OnChannelMessage(lacewing::relayserver& server, std::shared_ptr<lacewing::relayserver::client> senderclient,
std::shared_ptr<lacewing::relayserver::channel> channel,
bool blasted, lw_ui8 subchannel, std::string_view data, lw_ui8 variant);
void OnPeerMessage(lacewing::relayserver& server, std::shared_ptr<lacewing::relayserver::client> senderclient,
std::shared_ptr<lacewing::relayserver::channel> viachannel, std::shared_ptr<lacewing::relayserver::client> receiverclient,
bool blasted, lw_ui8 subchannel, std::string_view data, lw_ui8 variant);
// Declarations - functions
void GenerateFlashPolicy(int port);
void Shutdown();
void UpdateTitle(std::size_t clientCount);
// Ticker for liblacewing objects
lacewing::eventpump globalpump = nullptr;
// Blue server using Lacewing Relay protocol, consisting of main server,
// the Flash policy server, and WebSocket insecure and secure server.
lacewing::relayserver* globalserver = nullptr;
// If true, server has shut down. Only one shutdown request is sent.
bool shutdowned = false;
// Used for ticking stats over each second and updating status line
lacewing::timer globalmsgrecvcounttimer = nullptr;
void OnTimerTick(lacewing::timer timer);
// Normally app reports total statistics on end of app, then waits for keypress.
// If user closes server with Close on window, it must close ASAP.
// This is only used if isConsoleOutput is true.
bool waitOnExit = true;
// We use steady clock so server system time can change without messing with durations
using laceclock = std::chrono::steady_clock;
// Misbehaving user IPs. Used for bans and for counting infractions prior to a ban.
struct MisbehavingIPEntry
{
// IPv4 or [IPv6] address.
std::string ip;
// Number of disconnects, starts at 1 when user does something a little bad and gets kicked.
// If it reaches 3 the user cannot reconnect and is kicked.
// This is so user can misbehave a bit - for example, messaging the server something that server is
// not meant to answer - without getting a ban.
int disconnects;
// Ban reason, including the contact message.
std::string reason;
// Channel list at disconnect, only readable by sending report
std::string chListAtDisconnect;
// When the ban resets. Initial ban is one hour.
// Ban is extended by 1 hour if IP attempts to reconnect before resetAt.
// This time is only relevant if disconnects is greater than 3.
laceclock::time_point resetAt;
// Next time the console should display that this IP wes kicked for attempting to join with an active ban.
// Some lazy Fusion devs think if not connected, reconnect, and this would spam the console window.
// This starts at +1 minute from last log line, and is extended to +1 minute on each connect attempt.
laceclock::time_point nextLogLine;
MisbehavingIPEntry(const std::string_view ip, const int disconnects, const std::string_view reason,
const std::string_view chListAtDisconnect, const laceclock::time_point resetAt) :
ip(ip), disconnects(disconnects), reason(reason), chListAtDisconnect(chListAtDisconnect), resetAt(resetAt),
nextLogLine(laceclock::now())
{
// yay
}
};
// List of users that misbehaved at least once. Note that 3+ minor infractions, or 1 major, is needed for
// an IP to be unable to connect again.
// A minor infraction includes sending the server a direct message that it is not coded to receive,
// and exceeding the TCP upload limit.
// A major infraction is sending corrupt Lacewing messages, sending bad UTF-8, etc.
// Expired entries are only removed if the IP re-connects after entry expiry.
// Entries are not saved to disk on exit, so a server restart will clear them.
// To see entries at runtime, use the send report message.
static std::vector<MisbehavingIPEntry> misbehavingIPList;
// Statistic for message count and byte count
struct msgandbytestat
{
std::uint64_t msg = 0, bytes = 0;
msgandbytestat& operator += (const msgandbytestat& s) {
msg += s.msg;
bytes += s.bytes;
return *this;
}
void SetToMaxOfCurrentAndThis(const msgandbytestat& s) {
if (msg < s.msg)
msg = s.msg;
if (bytes < s.bytes)
bytes = s.bytes;
}
void AddMsg(const std::size_t msgSize)
{
++msg;
bytes += msgSize;
}
void AddMulti(const std::size_t msgCount, const std::size_t msgSize)
{
msg += msgCount;
bytes += msgSize * msgCount;
}
};
// Server-wide statistics, not kept across app close
struct {
struct {
msgandbytestat cur, lastSec, total, highestSec;
} in, out;
std::size_t maxClients = 0, maxChannels = 0;
} serverstats;
// Client-specific statistics
struct clientstats
{
std::shared_ptr<lacewing::relayserver::client> client;
std::size_t wastedServerMessages = 0;
msgandbytestat cur, lastSec, total, highestSec;
bool exceeded = false;
clientstats(std::shared_ptr<lacewing::relayserver::client> _c) : client(_c) {}
};
// Client-specific statistics for all current clients, created on connect
std::vector<std::shared_ptr<clientstats>> clientdata;
// Adds a misbehaving IP entry for a connected client. For unconnected clients, just add to misbehavingIPList directly.
void AddMisbehavingIPEntry(const clientstats& cliStats, const std::string_view addr, const std::string_view msg,
const laceclock::time_point banUntil)
{
std::stringstream chList;
{
const auto writeLock = cliStats.client->lock.createWriteLock();
for (auto p : cliStats.client->getchannels())
chList << '[' << p->name() << "], "sv;
}
std::string chListAtDisconnect = chList.str();
if (!chListAtDisconnect.empty())
chListAtDisconnect.resize(chListAtDisconnect.size() - 2);
else
chListAtDisconnect = "(empty)"sv;
misbehavingIPList.emplace_back(MisbehavingIPEntry(addr, 1, msg, chListAtDisconnect, banUntil));
}
// If true, stdout is a console handle. If false, stdout is a file, e.g. batch redirected to file.
// Disables things like asking for user input, and the maintained status line, to make the output more log-like.
bool isConsoleOutput = false;
// If true, asks users for input when not passed. If true, expects isConsoleOutput true.
bool requestUserInput = true;
// This value overrides requestUserInput calculation when a Windows debugger is attached.
// Useful when you're debugging the server and don't want a hundred prompts at start.
bool requestUserInputUnderDebugger = false;
// If false, isConsoleOutput is forced to false even with a full console.
// Output will be log-like, with no console text coloring, console title updates,
// or status line.
bool regularOutputEnabled = true;
// App folder, based on commandline
lw_string appFolder;
// Start time of server
std::time_t startTime;
decltype(misbehavingIPList)::const_iterator GetConstMisbehavingEntryByIP(const std::string_view& addr)
{
return std::find_if(misbehavingIPList.cbegin(), misbehavingIPList.cend(),
[&addr](const auto& e) { return e.ip == addr; });
}
decltype(misbehavingIPList)::iterator GetMisbehavingEntryByIP(const std::string_view& addr)
{
return std::find_if(misbehavingIPList.begin(), misbehavingIPList.end(),
[&addr](const auto& e) { return e.ip == addr; });
}
decltype(clientdata)::const_iterator ClientDataByClientPtr(const std::shared_ptr<lacewing::relayserver::client>& cli)
{
return std::find_if(clientdata.cbegin(), clientdata.cend(),
[&cli](const decltype(clientdata)::value_type & e) { return e->client == cli; });
}
// If true, next timer tick will dump statistics
// @remarks This technically should be made multi-thread safe, but in practice it's not a race condition that matters
bool statsDump;
// Statistics line kept at last line of console
lw_stringstream lastTimeAndStatsSS;
lw_string lastTimeAndStats;
// Wrapper to allow a distinct std::cout operator << overload
struct lineEndProp {
const bool reprintStatsLine;
};
// Pads the last output line and inserts newline, appending either just time prefix to next line,
// or reprinting whole statistics line
// @param reprintStatsLine if true, prints the statistics line again; if false, prints time only,
// expecting another line written immediately by caller
lineEndProp lineEnd(bool reprintStatsLine = true) {
return lineEndProp { reprintStatsLine };
}
// Colors console blue, expects isConsoleOutput true
template<class _Elem, class _Traits>
inline std::basic_ostream<_Elem, _Traits>& userpromptcolor(std::basic_ostream<_Elem, _Traits>& s)
{
// We use blue for user prompt, but we may skip listening for answer,
// so it is not always a console output
assert(isConsoleOutput);
// Only recolor if console output
if (isConsoleOutput)
{
#ifdef _WIN32
SetConsoleTextAttribute(hStdOut, FOREGROUND_BLUE | FOREGROUND_INTENSITY);
#else
s << "\033[94m";
#endif
}
// Always output time, console output or not
s << timeBuffer;
return s;
}
// Colors console white, expects isConsoleOutput true
template <class _Elem, class _Traits>
inline std::basic_ostream<_Elem, _Traits>& userresponsecolor(std::basic_ostream<_Elem, _Traits>& s)
{
// Responsibility of caller to ensure this is called only if stdout is a console.
// If stdout is redirected, waiting on std::cin will likely be indefinite.
// User input when not a console is commandline only.
assert(isConsoleOutput);
// Set color, and don't output time, or user will expect time to be live
#ifdef _WIN32
SetConsoleTextAttribute(hStdOut, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY);
#else
s << "\033[37m";
#endif
s << std::flush;
return s;
}
// Colors console red
template<class _Elem, class _Traits>
inline std::basic_ostream<_Elem, _Traits>& red(std::basic_ostream<_Elem, _Traits>& s)
{
// Only recolor if console output
if (isConsoleOutput)
{
#if lw_utf8_console
s << "\033[31m";
#else
SetConsoleTextAttribute(hStdOut, FOREGROUND_RED | FOREGROUND_INTENSITY);
#endif
}
// Always output time, console output or not
s << timeBuffer;
return s;
}
// Colors console green
template<class _Elem, class _Traits>
inline std::basic_ostream<_Elem, _Traits>& green(std::basic_ostream<_Elem, _Traits>& s)
{
// Only recolor if console output
if (isConsoleOutput)
{
#if lw_utf8_console
s << "\033[32m";
#else
SetConsoleTextAttribute(hStdOut, FOREGROUND_GREEN | FOREGROUND_INTENSITY);
#endif
}
// Always output time, console output or not
s << timeBuffer;
return s;
}
// Colors console yellow
template<class _Elem, class _Traits>
inline std::basic_ostream<_Elem, _Traits>& yellow(std::basic_ostream<_Elem, _Traits>& s)
{
// Only recolor if console output
if (isConsoleOutput)
{
#if lw_utf8_console
s << "\033[33m";
#else
SetConsoleTextAttribute(hStdOut, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY);
#endif
}
// Always output time, console output or not
s << timeBuffer;
return s;
}
// Colors console gray
template <class _Elem, class _Traits>
inline std::basic_ostream<_Elem, _Traits>& gray(std::basic_ostream<_Elem, _Traits>& s)
{
// Only recolor if console output
if (isConsoleOutput)
{
#if lw_utf8_console
s << "\033[37m";
#else
SetConsoleTextAttribute(hStdOut, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
#endif
}
// Always output time, console output or not
s << timeBuffer;
return s;
}
// Returns true if std::cin has a character to report. Uses OS-specific methods.
bool cinInputPending();