1 /*
2  * slashexec - A CLI for libpurple clients
3  * Copyright (C) 2004-2008 Gary Kramlich
4  * Copyright (C) 2005-2008 Peter Lawler
5  * Copyright (C) 2005-2008 Daniel Atallah
6  * Copyright (C) 2005-2008 John Bailey
7  * Copyright (C) 2006-2008 Sadrul Habib Chowdhury
8  *
9  * This program is free software; you can redistribute it and/or
10  * modify it under the terms of the GNU General Public License
11  * as published by the Free Software Foundation; either version 2
12  * of the License, or (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License
20  * along with this program; if not, write to the Free Software
21  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301, USA.
22  */
23 
24 /* XXX: Translations of strings - Several strings are NOT marked as needing to
25  * be translated.  This is because they go only to the debug window.  I only
26  * care to translate messages users will see in the main interface.  Debug
27  * window messages are not important. - rekkanoryo */
28 
29 /* If you can't figure out what this line is for, DON'T TOUCH IT. */
30 #include "../common/pp_internal.h"
31 
32 #include <stdarg.h>
33 #include <string.h>
34 
35 #ifndef _WIN32
36 # include <pwd.h>
37 # include <unistd.h>
38 #endif
39 
40 #include <cmds.h>
41 #include <conversation.h>
42 #include <debug.h>
43 #include <notify.h>
44 #include <plugin.h>
45 #include <util.h>
46 
47 #ifdef _WIN32
48 /* Windows 2000 and earlier allow only 2047 bytes in an argv vector for cmd.exe
49  * so we need to make sure we don't exceed that. 2036 allows "cmd.exe /c " to
50  * fit inside the vector. */
51 # define MAX_CMD_LEN 2036
52 #else
53 /* Some unixes allow only 8192 chars in an argv vector; allow some space for
54  * the shell command itself to fit in the vector.  Besides, if anyone is
55  * crazy enough to use /exec for a command this long they need shot to
56  * begin with, or educated on the beauty of shell scripts. */
57 # define MAX_CMD_LEN 8000
58 #endif
59 
60 #define PREF_PREFIX		"/plugins/core/slashexec"
61 #define PREF_SLASH		PREF_PREFIX "/slash"
62 #define PREF_BANG		PREF_PREFIX "/bang"
63 
64 static PurpleCmdId se_cmd;
65 static gchar *shell;
66 
67 static void /* replace a character at the end of a string with' \0' */
se_replace_ending_char(gchar * string,gchar replace)68 se_replace_ending_char(gchar *string, gchar replace)
69 {
70 	gint stringlen;
71 	gchar *replace_in_string;
72 
73 	stringlen = strlen(string);
74 	replace_in_string = g_utf8_strrchr(string, -1, replace);
75 
76 	/* continue to replace the character at the end of the string until the
77 	 * current last character in the string is not the character that needs to
78 	 * be replaced. */
79 	while(replace_in_string && replace_in_string == &(string[stringlen - 1])) {
80 		purple_debug_info("slashexec", "Replacing %c at position %d\n",
81 				replace, stringlen - 1);
82 
83 		/* a \0 is the logical end of the string, even if characters exist in
84 		 * the string after it.  Replacing the bad character with a \0 causes
85 		 * the string to technically be shortened by one character even though
86 		 * the allocated size of the string doesn't change. */
87 		*replace_in_string = '\0';
88 
89 		/* update the string length due to the new \0 */
90 		stringlen = strlen(string);
91 
92 		/* find, if one exists, another instance of the bad character */
93 		replace_in_string = g_utf8_strrchr(string, -1, replace);
94 	}
95 
96 	return;
97 }
98 
99 static gchar *
se_strdelimit(gchar * string,gchar newdelim)100 se_strdelimit(gchar *string, gchar newdelim)
101 { /* This function borrowed and tweaked from glib to suit my purposes */
102 
103 	/* these are the decimal representations of the ascii control characters that
104 	 * we need to remove to prevent bad behavior, such as XMPP disconnects */
105 	gchar delimiters[] = { 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 14, 15, 16, 17,
106 						18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 };
107 	gchar *c;
108 
109 	g_return_val_if_fail(string !=NULL, NULL);
110 
111 	for(c = string; *c; c = g_utf8_next_char(c))
112 		if(strchr(delimiters, *c))
113 			*c = newdelim;
114 
115 	return string;
116 }
117 
118 static gboolean
se_do_action(PurpleConversation * conv,gchar * args,gboolean send)119 se_do_action(PurpleConversation *conv, gchar *args, gboolean send)
120 {
121 	GError *parse_error = NULL, *exec_error = NULL;
122 	gchar *spawn_cmd = NULL, **cmd_argv = NULL, *cmd_stdout = NULL,
123 		  *cmd_stderr = NULL;
124 	gint cmd_argc = 0;
125 #ifdef _WIN32
126 	const gchar *shell_flag = "/c";
127 #else
128 	const gchar *shell_flag = "-c";
129 #endif
130 
131 	/* we can still end up with 0 "args", and we need to make sure we don't
132 	 * go over the max command length */
133 	if(strlen(args) == 0 || strlen(args) > MAX_CMD_LEN)
134 		return FALSE;
135 
136 	/* remove trailing \ characters to prevent some nasty crashes */
137 	se_replace_ending_char(args, '\\');
138 
139 #ifndef _WIN32
140 	/* escape remaining special characters; this is to ensure the shell gets
141 	 * what the user actually typed. */
142 	args = g_strescape(args, "");
143 #endif
144 
145 	/* if we get this far with a NULL, there's a problem. */
146 	if(!args) {
147 		purple_debug_info("slashexec", "args NULL!\n");
148 
149 		return FALSE;
150 	}
151 
152 	/* make sure the string passes the UTF8 validation in glib */
153 	if(!g_utf8_validate(args, -1, NULL)) {
154 		purple_debug_info("slashexec", "invalid UTF8: %s\n",
155 				args ? args : "null");
156 
157 		return FALSE;
158 	}
159 
160 #ifdef _WIN32
161 	spawn_cmd = g_strdup_printf("%s %s %s", shell, shell_flag, args);
162 #else
163 	spawn_cmd = g_strdup_printf("%s %s \"%s\"", shell, shell_flag, args);
164 #endif
165 
166 	/* We need the command parsed into a proper argv for it to be executed */
167 	if(!g_shell_parse_argv(spawn_cmd, &cmd_argc, &cmd_argv, &parse_error)) {
168 		/* everything in here is error checking and information for the user */
169 
170 		char *errmsg = NULL;
171 
172 		/* the command string isn't NULL, so give it to the user */
173 		if(spawn_cmd) {
174 			errmsg = g_strdup_printf(_("Unable to parse \"%s\""), spawn_cmd);
175 
176 			purple_debug_info("slashexec", "%s\n", errmsg);
177 			purple_conversation_write(conv, NULL, errmsg, PURPLE_MESSAGE_SYSTEM,
178 					time(NULL));
179 
180 			g_free(errmsg);
181 		}
182 
183 		/* the GError isn't NULL, so give its information to the user */
184 		if(parse_error) {
185 			errmsg = g_strdup_printf(_("Parse error message: %s"),
186 					parse_error->message ? parse_error->message : "null");
187 
188 			purple_debug_info("slashexec", "%s\n", errmsg);
189 			purple_conversation_write(conv, NULL, errmsg, PURPLE_MESSAGE_SYSTEM,
190 					time(NULL));
191 
192 			g_free(errmsg);
193 			g_error_free(parse_error);
194 		}
195 
196 		if(cmd_argv)
197 			g_strfreev(cmd_argv);
198 
199 		return FALSE;
200 	}
201 
202 	/* I may eventually add a pref to show this to the user; for now it's fine
203 	 * going just to the debug bucket */
204 	purple_debug_info("slashexec", "Spawn command: %s\n", spawn_cmd);
205 
206 	/* now we actually execute the command.  everything inside the block
207 	 * is error checking and information for the user. */
208 	if(!g_spawn_sync(NULL, cmd_argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
209 				&cmd_stdout, &cmd_stderr, NULL, &exec_error))
210 	{
211 		char *errmsg = NULL;
212 
213 		/* the command isn't NULL, so let the user see it */
214 		if(spawn_cmd) {
215 			errmsg = g_strdup_printf(_("Unable to execute \"%s\""),	spawn_cmd);
216 
217 			purple_debug_info("slashexec", "%s\n", errmsg);
218 			purple_conversation_write(conv, NULL, errmsg, PURPLE_MESSAGE_SYSTEM,
219 					time(NULL));
220 
221 			g_free(errmsg);
222 		}
223 
224 		/* the GError isn't NULL, so let's show the user its information */
225 		if(exec_error) {
226 			errmsg = g_strdup_printf(_("Execute error message: %s"),
227 					exec_error->message ? exec_error->message : "NULL");
228 
229 			purple_debug_info("slashexec", "%s\n", errmsg);
230 			purple_conversation_write(conv, NULL, errmsg,	PURPLE_MESSAGE_SYSTEM,
231 					time(NULL));
232 
233 			g_free(errmsg);
234 			g_error_free(exec_error);
235 		}
236 
237 		g_free(cmd_stdout);
238 		g_free(cmd_stderr);
239 		g_strfreev(cmd_argv);
240 
241 		return FALSE;
242 	}
243 
244 	/* if we get this far then the command actually executed */
245 	if(parse_error)
246 		g_error_free(parse_error);
247 
248 	if(exec_error)
249 		g_error_free(exec_error);
250 
251 	if(cmd_stderr)
252 		purple_debug_info("slashexec", "command stderr: %s\n", cmd_stderr);
253 
254 	g_strfreev(cmd_argv);
255 	g_free(cmd_stderr);
256 
257 	if(cmd_stdout && strcmp(cmd_stdout, "") && strcmp(cmd_stdout, "\n")) {
258 		g_strchug(cmd_stdout);
259 
260 		if(g_str_has_suffix(cmd_stdout, "\n"))
261 			cmd_stdout[strlen(cmd_stdout) - 1] = '\0';
262 
263 		if(send) {
264 			purple_debug_info("slashexec", "Command stdout: %s\n", cmd_stdout);
265 
266 			if(!g_utf8_validate(cmd_stdout, -1, NULL)) {
267 				purple_debug_error("slashexec", "Output failed UTF-8 verification!\n");
268 
269 				return FALSE;
270 			} else {
271 				cmd_stdout = se_strdelimit(cmd_stdout, ' ');
272 
273 				g_strstrip(cmd_stdout);
274 
275 				purple_debug_info("slashexec", "Sanitized command stdout: %s\n", cmd_stdout);
276 			}
277 
278 			switch(purple_conversation_get_type(conv)) {
279 				case PURPLE_CONV_TYPE_IM:
280 					purple_conv_im_send(PURPLE_CONV_IM(conv), cmd_stdout);
281 					break;
282 				case PURPLE_CONV_TYPE_CHAT:
283 					purple_conv_chat_send(PURPLE_CONV_CHAT(conv), cmd_stdout);
284 					break;
285 				default:
286 					return FALSE;
287 			}
288 
289 		} else
290 			purple_conversation_write(conv, NULL, cmd_stdout, PURPLE_MESSAGE_SYSTEM,
291 					time(NULL));
292 	} else {
293 		purple_debug_info("slashexec", "Error executing \"%s\"\n", spawn_cmd);
294 		purple_conversation_write(conv, NULL,
295 				_("There was an error executing your command."),
296 				PURPLE_MESSAGE_SYSTEM, time(NULL));
297 
298 		g_free(spawn_cmd);
299 		g_free(cmd_stdout);
300 
301 		return FALSE;
302 	}
303 
304 	g_free(cmd_stdout);
305 	g_free(spawn_cmd);
306 
307 	return TRUE;
308 }
309 
310 static PurpleCmdRet
se_cmd_cb(PurpleConversation * conv,const gchar * cmd,gchar ** args,gchar ** error,gpointer data)311 se_cmd_cb(PurpleConversation *conv, const gchar *cmd, gchar **args, gchar **error,
312 			gpointer data)
313 {
314 	gboolean send = FALSE;
315 	char *string = args[0];
316 
317 	if(!purple_prefs_get_bool(PREF_SLASH))
318 		return PURPLE_CMD_RET_CONTINUE;
319 
320 	if(string && !strncmp(string, "-o ", 3)) {
321 		send = TRUE;
322 		string += 3;
323 	}
324 
325 	if(se_do_action(conv, string, send))
326 		return PURPLE_CMD_RET_OK;
327 	else
328 		return PURPLE_CMD_RET_FAILED;
329 }
330 
331 static void
se_sending_msg_helper(PurpleConversation * conv,char ** message)332 se_sending_msg_helper(PurpleConversation *conv, char **message)
333 {
334 	char *string = *message, *strip = NULL;
335 	gboolean send = TRUE;
336 
337 	if(conv == NULL || !purple_prefs_get_bool(PREF_BANG)) return;
338 
339 	strip = purple_markup_strip_html(string);
340 
341 	if(strip && *strip != '!') {
342 		g_free(strip);
343 		return;
344 	}
345 
346 	*message = NULL;
347 
348 	g_free(string);
349 
350 	/* this is refactored quite a bit to simplify things since sending-im-msg
351 	 * allows changing the text that will be sent by simply giving *message a
352 	 * new pointer */
353 	if(strncmp(strip, "!!!", 3) == 0) {
354 		char *new_msg, *conv_sys_msg;
355 
356 		new_msg = g_strdup(strip + 2);
357 
358 		*message = new_msg;
359 
360 		/* I really want to eventually make this cleaner, like by making it
361 		 * change the actual message that gets printed to the conv window... */
362 		conv_sys_msg = g_strdup_printf(_("The following text was sent: %s"),
363 				new_msg);
364 
365 		purple_conversation_write(conv, NULL, conv_sys_msg, PURPLE_MESSAGE_SYSTEM,
366 				time(NULL));
367 
368 		g_free(strip);
369 		g_free(conv_sys_msg);
370 
371 		return;
372 	}
373 
374 	if(*(strip + 1) == '!')
375 		send = FALSE;
376 
377 	if(send)
378 		se_do_action(conv, strip + 1, send);
379 	else
380 		se_do_action(conv, strip + 2, send);
381 
382 	g_free(strip);
383 }
384 
385 static void
se_sending_chat_msg_cb(PurpleAccount * account,char ** message,int id)386 se_sending_chat_msg_cb(PurpleAccount *account, char **message, int id)
387 {
388 	PurpleConversation *conv;
389 	if (message == NULL || *message == NULL)  /* It's possible if some other callback to
390 	                                           to the signal resets the message */
391 		return;
392 	conv = purple_find_chat(account->gc, id);
393 	se_sending_msg_helper(conv, message);
394 }
395 
396 static void
se_sending_im_msg_cb(PurpleAccount * account,const char * who,char ** message)397 se_sending_im_msg_cb(PurpleAccount *account, const char *who, char **message)
398 {
399 	PurpleConversation *conv;
400 	if (message == NULL || *message == NULL)  /* It's possible if some other callback to
401 												 to the signal resets the message */
402 		return;
403 	conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, who, account);
404 	se_sending_msg_helper(conv, message);
405 }
406 
407 static gboolean
se_load(PurplePlugin * plugin)408 se_load(PurplePlugin *plugin) {
409 #ifndef _WIN32
410 	struct passwd *pw = NULL;
411 #endif /* _WIN32 */
412 	const gchar *help = _("exec [-o] &lt;command&gt;, runs the command.\n"
413 						"If the -o flag is used then output is sent to the"
414 						"current conversation; otherwise it is printed to the "
415 						"current text box.");
416 
417 	se_cmd = purple_cmd_register("exec", "s", PURPLE_CMD_P_PLUGIN,
418 								PURPLE_CMD_FLAG_IM | PURPLE_CMD_FLAG_CHAT, NULL,
419 								se_cmd_cb, help, NULL);
420 
421 	/* these signals are needed for the bangexec features we took on */
422 	purple_signal_connect(purple_conversations_get_handle(), "sending-im-msg", plugin,
423 				PURPLE_CALLBACK(se_sending_im_msg_cb), NULL);
424 	purple_signal_connect(purple_conversations_get_handle(), "sending-chat-msg", plugin,
425 				PURPLE_CALLBACK(se_sending_chat_msg_cb), NULL);
426 
427 	/* this gets the user's shell.  If this is built on Windows, force cmd.exe,
428 	 * which will make this plugin not work on Windows 98/ME */
429 #ifdef _WIN32
430 	shell = g_strdup("cmd.exe");
431 #else
432 	pw = getpwuid(getuid());
433 
434 	if(pw)
435 		if(pw->pw_shell)
436 			shell = g_strdup(pw->pw_shell);
437 		else
438 			shell = g_strdup("/bin/sh");
439 	else
440 		shell = g_strdup("/bin/sh");
441 #endif /* _WIN32 */
442 
443 	return TRUE;
444 }
445 
446 static gboolean
se_unload(PurplePlugin * plugin)447 se_unload(PurplePlugin *plugin) {
448 	purple_cmd_unregister(se_cmd);
449 
450 	g_free(shell);
451 
452 	return TRUE;
453 }
454 
455 static PurplePluginPrefFrame *
get_plugin_pref_frame(PurplePlugin * plugin)456 get_plugin_pref_frame(PurplePlugin *plugin)
457 {
458 	PurplePluginPrefFrame *frame;
459 	PurplePluginPref *pref;
460 
461 	frame = purple_plugin_pref_frame_new();
462 
463 	pref = purple_plugin_pref_new_with_label(_("Execute commands starting with: "));
464 	purple_plugin_pref_frame_add(frame, pref);
465 
466 	pref = purple_plugin_pref_new_with_name_and_label(PREF_SLASH,
467 					_("/exec Command (/exec someCommand)"));
468 	purple_plugin_pref_frame_add(frame, pref);
469 
470 	pref = purple_plugin_pref_new_with_name_and_label(PREF_BANG,
471 					_("Exclamation point (!someCommand)"));
472 	purple_plugin_pref_frame_add(frame, pref);
473 
474 	return frame;
475 }
476 
477 static PurplePluginUiInfo prefs_info = {
478 	get_plugin_pref_frame,
479 	0,
480 	NULL,
481 
482 	/* padding */
483 	NULL,
484 	NULL,
485 	NULL,
486 	NULL
487 };
488 
489 static PurplePluginInfo se_info = {
490 	PURPLE_PLUGIN_MAGIC,
491 	PURPLE_MAJOR_VERSION,
492 	PURPLE_MINOR_VERSION,
493 	PURPLE_PLUGIN_STANDARD,								/* type				*/
494 	NULL,												/* ui requirement	*/
495 	0,													/* flags			*/
496 	NULL,												/* dependencies		*/
497 	PURPLE_PRIORITY_DEFAULT,							/* priority			*/
498 	"core-plugin_pack-slashexec",						/* id				*/
499 	"/exec",											/* name				*/
500 	PP_VERSION,											/* version			*/
501 	NULL,												/* summary			*/
502 	NULL,												/* description		*/
503 
504 	"Gary Kramlich <grim@reaperworld.com>,\n"
505 	"Peter Lawler <bleeter from users.sf.net>,\n"
506 	"Daniel Atallah,\nSadrul Habib Chowdhury <sadrul@users.sf.net>,\n"
507 	"John Bailey <rekkanoryo@rekkanoryo.org>",			/* authors			*/
508 
509 	PP_WEBSITE,											/* homepage			*/
510 	se_load,											/* load				*/
511 	se_unload,											/* unload			*/
512 	NULL,												/* destroy			*/
513 	NULL,												/* ui info			*/
514 	NULL,												/* extra info		*/
515 	&prefs_info,										/* prefs info		*/
516 	NULL,												/* actions info		*/
517 	NULL,
518 	NULL,
519 	NULL,
520 	NULL
521 };
522 
523 static void
init_plugin(PurplePlugin * plugin)524 init_plugin(PurplePlugin *plugin) {
525 #ifdef ENABLE_NLS
526 	bindtextdomain(GETTEXT_PACKAGE, PP_LOCALEDIR);
527 	bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
528 #endif /* ENABLE_NLS */
529 	se_info.summary = _("/exec a la UNIX IRC CLI");
530 	se_info.description = _("A plugin that adds the /exec command line"
531 							" interpreter like most UNIX/Linux IRC"
532 							" clients have.  Also included is the ability to"
533 							" execute commands with an exclamation point"
534 							" (!uptime, for instance).\n");
535 	purple_prefs_add_none(PREF_PREFIX);
536 	purple_prefs_add_bool(PREF_SLASH, TRUE);
537 	purple_prefs_add_bool(PREF_BANG, FALSE);
538 }
539 
540 PURPLE_INIT_PLUGIN(slashexec, init_plugin, se_info)
541