1 /*
2  * This file is part of OpenTTD.
3  * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4  * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5  * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
6  */
7 
8 /** @file console.cpp Handling of the in-game console. */
9 
10 #include "stdafx.h"
11 #include "console_internal.h"
12 #include "network/network.h"
13 #include "network/network_func.h"
14 #include "network/network_admin.h"
15 #include "debug.h"
16 #include "console_func.h"
17 #include "settings_type.h"
18 
19 #include <stdarg.h>
20 
21 #include "safeguards.h"
22 
23 static const uint ICON_TOKEN_COUNT = 20;     ///< Maximum number of tokens in one command
24 static const uint ICON_MAX_RECURSE = 10;     ///< Maximum number of recursion
25 
26 /* console parser */
Commands()27 /* static */ IConsole::CommandList &IConsole::Commands()
28 {
29 	static IConsole::CommandList cmds;
30 	return cmds;
31 }
32 
Aliases()33 /* static */ IConsole::AliasList &IConsole::Aliases()
34 {
35 	static IConsole::AliasList aliases;
36 	return aliases;
37 }
38 
39 FILE *_iconsole_output_file;
40 
IConsoleInit()41 void IConsoleInit()
42 {
43 	_iconsole_output_file = nullptr;
44 	_redirect_console_to_client = INVALID_CLIENT_ID;
45 	_redirect_console_to_admin  = INVALID_ADMIN_ID;
46 
47 	IConsoleGUIInit();
48 
49 	IConsoleStdLibRegister();
50 }
51 
IConsoleWriteToLogFile(const char * string)52 static void IConsoleWriteToLogFile(const char *string)
53 {
54 	if (_iconsole_output_file != nullptr) {
55 		/* if there is an console output file ... also print it there */
56 		const char *header = GetLogPrefix();
57 		if ((strlen(header) != 0 && fwrite(header, strlen(header), 1, _iconsole_output_file) != 1) ||
58 				fwrite(string, strlen(string), 1, _iconsole_output_file) != 1 ||
59 				fwrite("\n", 1, 1, _iconsole_output_file) != 1) {
60 			fclose(_iconsole_output_file);
61 			_iconsole_output_file = nullptr;
62 			IConsolePrint(CC_ERROR, "Cannot write to console log file; closing the log file.");
63 		}
64 	}
65 }
66 
CloseConsoleLogIfActive()67 bool CloseConsoleLogIfActive()
68 {
69 	if (_iconsole_output_file != nullptr) {
70 		IConsolePrint(CC_INFO, "Console log file closed.");
71 		fclose(_iconsole_output_file);
72 		_iconsole_output_file = nullptr;
73 		return true;
74 	}
75 
76 	return false;
77 }
78 
IConsoleFree()79 void IConsoleFree()
80 {
81 	IConsoleGUIFree();
82 	CloseConsoleLogIfActive();
83 }
84 
85 /**
86  * Handle the printing of text entered into the console or redirected there
87  * by any other means. Text can be redirected to other clients in a network game
88  * as well as to a logfile. If the network server is a dedicated server, all activities
89  * are also logged. All lines to print are added to a temporary buffer which can be
90  * used as a history to print them onscreen
91  * @param colour_code The colour of the command.
92  * @param string The message to output on the console (notice, error, etc.)
93  */
IConsolePrint(TextColour colour_code,const std::string & string)94 void IConsolePrint(TextColour colour_code, const std::string &string)
95 {
96 	assert(IsValidConsoleColour(colour_code));
97 
98 	if (_redirect_console_to_client != INVALID_CLIENT_ID) {
99 		/* Redirect the string to the client */
100 		NetworkServerSendRcon(_redirect_console_to_client, colour_code, string);
101 		return;
102 	}
103 
104 	if (_redirect_console_to_admin != INVALID_ADMIN_ID) {
105 		NetworkServerSendAdminRcon(_redirect_console_to_admin, colour_code, string);
106 		return;
107 	}
108 
109 	/* Create a copy of the string, strip if of colours and invalid
110 	 * characters and (when applicable) assign it to the console buffer */
111 	char *str = stredup(string.c_str());
112 	str_strip_colours(str);
113 	StrMakeValidInPlace(str);
114 
115 	if (_network_dedicated) {
116 		NetworkAdminConsole("console", str);
117 		fprintf(stdout, "%s%s\n", GetLogPrefix(), str);
118 		fflush(stdout);
119 		IConsoleWriteToLogFile(str);
120 		free(str); // free duplicated string since it's not used anymore
121 		return;
122 	}
123 
124 	IConsoleWriteToLogFile(str);
125 	IConsoleGUIPrint(colour_code, str);
126 }
127 
128 /**
129  * Change a string into its number representation. Supports
130  * decimal and hexadecimal numbers as well as 'on'/'off' 'true'/'false'
131  * @param *value the variable a successful conversion will be put in
132  * @param *arg the string to be converted
133  * @return Return true on success or false on failure
134  */
GetArgumentInteger(uint32 * value,const char * arg)135 bool GetArgumentInteger(uint32 *value, const char *arg)
136 {
137 	char *endptr;
138 
139 	if (strcmp(arg, "on") == 0 || strcmp(arg, "true") == 0) {
140 		*value = 1;
141 		return true;
142 	}
143 	if (strcmp(arg, "off") == 0 || strcmp(arg, "false") == 0) {
144 		*value = 0;
145 		return true;
146 	}
147 
148 	*value = strtoul(arg, &endptr, 0);
149 	return arg != endptr;
150 }
151 
152 /**
153  * Creates a copy of a string with underscores removed from it
154  * @param name String to remove the underscores from.
155  * @return A copy of \a name, without underscores.
156  */
RemoveUnderscores(std::string name)157 static std::string RemoveUnderscores(std::string name)
158 {
159 	name.erase(std::remove(name.begin(), name.end(), '_'), name.end());
160 	return name;
161 }
162 
163 /**
164  * Register a new command to be used in the console
165  * @param name name of the command that will be used
166  * @param proc function that will be called upon execution of command
167  */
CmdRegister(const std::string & name,IConsoleCmdProc * proc,IConsoleHook * hook)168 /* static */ void IConsole::CmdRegister(const std::string &name, IConsoleCmdProc *proc, IConsoleHook *hook)
169 {
170 	IConsole::Commands().try_emplace(RemoveUnderscores(name), name, proc, hook);
171 }
172 
173 /**
174  * Find the command pointed to by its string
175  * @param name command to be found
176  * @return return Cmdstruct of the found command, or nullptr on failure
177  */
CmdGet(const std::string & name)178 /* static */ IConsoleCmd *IConsole::CmdGet(const std::string &name)
179 {
180 	auto item = IConsole::Commands().find(RemoveUnderscores(name));
181 	if (item != IConsole::Commands().end()) return &item->second;
182 	return nullptr;
183 }
184 
185 /**
186  * Register a an alias for an already existing command in the console
187  * @param name name of the alias that will be used
188  * @param cmd name of the command that 'name' will be alias of
189  */
AliasRegister(const std::string & name,const std::string & cmd)190 /* static */ void IConsole::AliasRegister(const std::string &name, const std::string &cmd)
191 {
192 	auto result = IConsole::Aliases().try_emplace(RemoveUnderscores(name), name, cmd);
193 	if (!result.second) IConsolePrint(CC_ERROR, "An alias with the name '{}' already exists.", name);
194 }
195 
196 /**
197  * Find the alias pointed to by its string
198  * @param name alias to be found
199  * @return return Aliasstruct of the found alias, or nullptr on failure
200  */
AliasGet(const std::string & name)201 /* static */ IConsoleAlias *IConsole::AliasGet(const std::string &name)
202 {
203 	auto item = IConsole::Aliases().find(RemoveUnderscores(name));
204 	if (item != IConsole::Aliases().end()) return &item->second;
205 	return nullptr;
206 }
207 
208 /**
209  * An alias is just another name for a command, or for more commands
210  * Execute it as well.
211  * @param *alias is the alias of the command
212  * @param tokencount the number of parameters passed
213  * @param *tokens are the parameters given to the original command (0 is the first param)
214  */
IConsoleAliasExec(const IConsoleAlias * alias,byte tokencount,char * tokens[ICON_TOKEN_COUNT],const uint recurse_count)215 static void IConsoleAliasExec(const IConsoleAlias *alias, byte tokencount, char *tokens[ICON_TOKEN_COUNT], const uint recurse_count)
216 {
217 	char  alias_buffer[ICON_MAX_STREAMSIZE] = { '\0' };
218 	char *alias_stream = alias_buffer;
219 
220 	Debug(console, 6, "Requested command is an alias; parsing...");
221 
222 	if (recurse_count > ICON_MAX_RECURSE) {
223 		IConsolePrint(CC_ERROR, "Too many alias expansions, recursion limit reached.");
224 		return;
225 	}
226 
227 	for (const char *cmdptr = alias->cmdline.c_str(); *cmdptr != '\0'; cmdptr++) {
228 		switch (*cmdptr) {
229 			case '\'': // ' will double for ""
230 				alias_stream = strecpy(alias_stream, "\"", lastof(alias_buffer));
231 				break;
232 
233 			case ';': // Cmd separator; execute previous and start new command
234 				IConsoleCmdExec(alias_buffer, recurse_count);
235 
236 				alias_stream = alias_buffer;
237 				*alias_stream = '\0'; // Make sure the new command is terminated.
238 
239 				cmdptr++;
240 				break;
241 
242 			case '%': // Some or all parameters
243 				cmdptr++;
244 				switch (*cmdptr) {
245 					case '+': { // All parameters separated: "[param 1]" "[param 2]"
246 						for (uint i = 0; i != tokencount; i++) {
247 							if (i != 0) alias_stream = strecpy(alias_stream, " ", lastof(alias_buffer));
248 							alias_stream = strecpy(alias_stream, "\"", lastof(alias_buffer));
249 							alias_stream = strecpy(alias_stream, tokens[i], lastof(alias_buffer));
250 							alias_stream = strecpy(alias_stream, "\"", lastof(alias_buffer));
251 						}
252 						break;
253 					}
254 
255 					case '!': { // Merge the parameters to one: "[param 1] [param 2] [param 3...]"
256 						alias_stream = strecpy(alias_stream, "\"", lastof(alias_buffer));
257 						for (uint i = 0; i != tokencount; i++) {
258 							if (i != 0) alias_stream = strecpy(alias_stream, " ", lastof(alias_buffer));
259 							alias_stream = strecpy(alias_stream, tokens[i], lastof(alias_buffer));
260 						}
261 						alias_stream = strecpy(alias_stream, "\"", lastof(alias_buffer));
262 						break;
263 					}
264 
265 					default: { // One specific parameter: %A = [param 1] %B = [param 2] ...
266 						int param = *cmdptr - 'A';
267 
268 						if (param < 0 || param >= tokencount) {
269 							IConsolePrint(CC_ERROR, "Too many or wrong amount of parameters passed to alias.");
270 							IConsolePrint(CC_HELP, "Usage of alias '{}': '{}'.", alias->name, alias->cmdline);
271 							return;
272 						}
273 
274 						alias_stream = strecpy(alias_stream, "\"", lastof(alias_buffer));
275 						alias_stream = strecpy(alias_stream, tokens[param], lastof(alias_buffer));
276 						alias_stream = strecpy(alias_stream, "\"", lastof(alias_buffer));
277 						break;
278 					}
279 				}
280 				break;
281 
282 			default:
283 				*alias_stream++ = *cmdptr;
284 				*alias_stream = '\0';
285 				break;
286 		}
287 
288 		if (alias_stream >= lastof(alias_buffer) - 1) {
289 			IConsolePrint(CC_ERROR, "Requested alias execution would overflow execution buffer.");
290 			return;
291 		}
292 	}
293 
294 	IConsoleCmdExec(alias_buffer, recurse_count);
295 }
296 
297 /**
298  * Execute a given command passed to us. First chop it up into
299  * individual tokens (separated by spaces), then execute it if possible
300  * @param cmdstr string to be parsed and executed
301  */
IConsoleCmdExec(const char * cmdstr,const uint recurse_count)302 void IConsoleCmdExec(const char *cmdstr, const uint recurse_count)
303 {
304 	const char *cmdptr;
305 	char *tokens[ICON_TOKEN_COUNT], tokenstream[ICON_MAX_STREAMSIZE];
306 	uint t_index, tstream_i;
307 
308 	bool longtoken = false;
309 	bool foundtoken = false;
310 
311 	if (cmdstr[0] == '#') return; // comments
312 
313 	for (cmdptr = cmdstr; *cmdptr != '\0'; cmdptr++) {
314 		if (!IsValidChar(*cmdptr, CS_ALPHANUMERAL)) {
315 			IConsolePrint(CC_ERROR, "Command '{}' contains malformed characters.", cmdstr);
316 			return;
317 		}
318 	}
319 
320 	Debug(console, 4, "Executing cmdline: '{}'", cmdstr);
321 
322 	memset(&tokens, 0, sizeof(tokens));
323 	memset(&tokenstream, 0, sizeof(tokenstream));
324 
325 	/* 1. Split up commandline into tokens, separated by spaces, commands
326 	 * enclosed in "" are taken as one token. We can only go as far as the amount
327 	 * of characters in our stream or the max amount of tokens we can handle */
328 	for (cmdptr = cmdstr, t_index = 0, tstream_i = 0; *cmdptr != '\0'; cmdptr++) {
329 		if (tstream_i >= lengthof(tokenstream)) {
330 			IConsolePrint(CC_ERROR, "Command line too long.");
331 			return;
332 		}
333 
334 		switch (*cmdptr) {
335 		case ' ': // Token separator
336 			if (!foundtoken) break;
337 
338 			if (longtoken) {
339 				tokenstream[tstream_i] = *cmdptr;
340 			} else {
341 				tokenstream[tstream_i] = '\0';
342 				foundtoken = false;
343 			}
344 
345 			tstream_i++;
346 			break;
347 		case '"': // Tokens enclosed in "" are one token
348 			longtoken = !longtoken;
349 			if (!foundtoken) {
350 				if (t_index >= lengthof(tokens)) {
351 					IConsolePrint(CC_ERROR, "Command line too long.");
352 					return;
353 				}
354 				tokens[t_index++] = &tokenstream[tstream_i];
355 				foundtoken = true;
356 			}
357 			break;
358 		case '\\': // Escape character for ""
359 			if (cmdptr[1] == '"' && tstream_i + 1 < lengthof(tokenstream)) {
360 				tokenstream[tstream_i++] = *++cmdptr;
361 				break;
362 			}
363 			FALLTHROUGH;
364 		default: // Normal character
365 			tokenstream[tstream_i++] = *cmdptr;
366 
367 			if (!foundtoken) {
368 				if (t_index >= lengthof(tokens)) {
369 					IConsolePrint(CC_ERROR, "Command line too long.");
370 					return;
371 				}
372 				tokens[t_index++] = &tokenstream[tstream_i - 1];
373 				foundtoken = true;
374 			}
375 			break;
376 		}
377 	}
378 
379 	for (uint i = 0; i < lengthof(tokens) && tokens[i] != nullptr; i++) {
380 		Debug(console, 8, "Token {} is: '{}'", i, tokens[i]);
381 	}
382 
383 	if (StrEmpty(tokens[0])) return; // don't execute empty commands
384 	/* 2. Determine type of command (cmd or alias) and execute
385 	 * First try commands, then aliases. Execute
386 	 * the found action taking into account its hooking code
387 	 */
388 	IConsoleCmd *cmd = IConsole::CmdGet(tokens[0]);
389 	if (cmd != nullptr) {
390 		ConsoleHookResult chr = (cmd->hook == nullptr ? CHR_ALLOW : cmd->hook(true));
391 		switch (chr) {
392 			case CHR_ALLOW:
393 				if (!cmd->proc(t_index, tokens)) { // index started with 0
394 					cmd->proc(0, nullptr); // if command failed, give help
395 				}
396 				return;
397 
398 			case CHR_DISALLOW: return;
399 			case CHR_HIDE: break;
400 		}
401 	}
402 
403 	t_index--;
404 	IConsoleAlias *alias = IConsole::AliasGet(tokens[0]);
405 	if (alias != nullptr) {
406 		IConsoleAliasExec(alias, t_index, &tokens[1], recurse_count + 1);
407 		return;
408 	}
409 
410 	IConsolePrint(CC_ERROR, "Command '{}' not found.", tokens[0]);
411 }
412