| |
1 /* |
| |
2 * Purple - Internet Messaging Library |
| |
3 * Copyright (C) Pidgin Developers <devel@pidgin.im> |
| |
4 * |
| |
5 * Purple is the legal property of its developers, whose names are too numerous |
| |
6 * to list here. Please refer to the COPYRIGHT file distributed with this |
| |
7 * source distribution. |
| |
8 * |
| |
9 * This library is free software; you can redistribute it and/or modify |
| |
10 * it under the terms of the GNU General Public License as published by |
| |
11 * the Free Software Foundation; either version 2 of the License, or |
| |
12 * (at your option) any later version. |
| |
13 * |
| |
14 * This library 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 library; if not, see <https://www.gnu.org/licenses/>. |
| |
21 */ |
| |
22 |
| |
23 #include "purpleircv3parser.h" |
| |
24 |
| |
25 #include "purpleircv3capabilities.h" |
| |
26 #include "purpleircv3constants.h" |
| |
27 #include "purpleircv3core.h" |
| |
28 #include "purpleircv3message.h" |
| |
29 #include "purpleircv3messagehandlers.h" |
| |
30 #include "purpleircv3sasl.h" |
| |
31 |
| |
32 struct _PurpleIRCv3Parser { |
| |
33 GObject parent; |
| |
34 |
| |
35 GRegex *regex_message; |
| |
36 GRegex *regex_tags; |
| |
37 |
| |
38 PurpleIRCv3MessageHandler fallback_handler; |
| |
39 GHashTable *handlers; |
| |
40 }; |
| |
41 |
| |
42 G_DEFINE_FINAL_TYPE(PurpleIRCv3Parser, purple_ircv3_parser, G_TYPE_OBJECT) |
| |
43 |
| |
44 /****************************************************************************** |
| |
45 * Helpers |
| |
46 *****************************************************************************/ |
| |
47 static char * |
| |
48 purple_ircv3_parser_unescape_tag_value(const char *value) { |
| |
49 GString *unescaped = g_string_new(""); |
| |
50 gboolean escaping = FALSE; |
| |
51 |
| |
52 /* Walk the string and replace escaped values according to |
| |
53 * https://ircv3.net/specs/extensions/message-tags.html#escaping-values |
| |
54 */ |
| |
55 for(int i = 0; value[i] != '\0'; i++) { |
| |
56 if(escaping) { |
| |
57 /* Set the replacement to the current character which will fall |
| |
58 * through for everything, including '\\' |
| |
59 */ |
| |
60 char replacement = value[i]; |
| |
61 |
| |
62 if(value[i] == ':') { |
| |
63 replacement = ';'; |
| |
64 } else if(value[i] == 's') { |
| |
65 replacement = ' '; |
| |
66 } else if(value[i] == 'r') { |
| |
67 replacement = '\r'; |
| |
68 } else if(value[i] == 'n') { |
| |
69 replacement = '\n'; |
| |
70 } |
| |
71 |
| |
72 g_string_append_c(unescaped, replacement); |
| |
73 escaping = FALSE; |
| |
74 } else { |
| |
75 if(value[i] == '\\') { |
| |
76 escaping = TRUE; |
| |
77 } else { |
| |
78 g_string_append_c(unescaped, value[i]); |
| |
79 } |
| |
80 } |
| |
81 } |
| |
82 |
| |
83 return g_string_free(unescaped, FALSE); |
| |
84 } |
| |
85 |
| |
86 static GHashTable * |
| |
87 purple_ircv3_parser_parse_tags(PurpleIRCv3Parser *parser, |
| |
88 const gchar *tags_string, GError **error) |
| |
89 { |
| |
90 GError *local_error = NULL; |
| |
91 GHashTable *tags = NULL; |
| |
92 GMatchInfo *info = NULL; |
| |
93 gboolean matches = FALSE; |
| |
94 |
| |
95 tags = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); |
| |
96 |
| |
97 /* tags_string can never be NULL, because g_match_info_fetch_named always |
| |
98 * returns a string. So if we were passed an empty string, just return the |
| |
99 * empty hash table. |
| |
100 */ |
| |
101 if(*tags_string == '\0') { |
| |
102 return tags; |
| |
103 } |
| |
104 |
| |
105 matches = g_regex_match_full(parser->regex_tags, tags_string, -1, 0, 0, |
| |
106 &info, &local_error); |
| |
107 |
| |
108 if(local_error != NULL) { |
| |
109 g_propagate_error(error, local_error); |
| |
110 |
| |
111 g_match_info_unref(info); |
| |
112 |
| |
113 return tags; |
| |
114 } |
| |
115 |
| |
116 if(!matches) { |
| |
117 g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0, |
| |
118 "failed to parse tags: unknown error"); |
| |
119 |
| |
120 g_match_info_unref(info); |
| |
121 |
| |
122 return tags; |
| |
123 } |
| |
124 |
| |
125 while(g_match_info_matches(info)) { |
| |
126 char *key = NULL; |
| |
127 char *value = NULL; |
| |
128 char *unescaped = NULL; |
| |
129 |
| |
130 key = g_match_info_fetch_named(info, "key"); |
| |
131 value = g_match_info_fetch_named(info, "value"); |
| |
132 |
| |
133 unescaped = purple_ircv3_parser_unescape_tag_value(value); |
| |
134 g_free(value); |
| |
135 |
| |
136 /* the hash table is created with destroy notifies for both key and |
| |
137 * value, so there's no need to free the allocated memory right now. |
| |
138 */ |
| |
139 g_hash_table_insert(tags, key, unescaped); |
| |
140 g_match_info_next(info, &local_error); |
| |
141 |
| |
142 if(local_error != NULL) { |
| |
143 g_propagate_error(error, local_error); |
| |
144 |
| |
145 break; |
| |
146 } |
| |
147 } |
| |
148 |
| |
149 g_match_info_unref(info); |
| |
150 |
| |
151 return tags; |
| |
152 } |
| |
153 |
| |
154 static guint |
| |
155 purple_ircv3_parser_extract_params(G_GNUC_UNUSED PurpleIRCv3Parser *parser, |
| |
156 GStrvBuilder *builder, const gchar *str) |
| |
157 { |
| |
158 gchar *ptr = NULL; |
| |
159 guint count = 0; |
| |
160 |
| |
161 /* Loop through str finding each space separated string. */ |
| |
162 while(str != NULL && *str != '\0') { |
| |
163 /* Look for a space. */ |
| |
164 ptr = strchr(str, ' '); |
| |
165 |
| |
166 /* If we found one, set it to null terminator and add the string to our |
| |
167 * builder. |
| |
168 */ |
| |
169 if(ptr != NULL) { |
| |
170 *ptr = '\0'; |
| |
171 g_strv_builder_add(builder, str); |
| |
172 |
| |
173 /* Move str to the next character as we know there's another |
| |
174 * character which might be another null terminator. |
| |
175 */ |
| |
176 str = ptr + 1; |
| |
177 |
| |
178 /* And don't forget to increment the count... ah ah ah! */ |
| |
179 count++; |
| |
180 } else { |
| |
181 /* Add the remaining string. */ |
| |
182 g_strv_builder_add(builder, str); |
| |
183 |
| |
184 /* Give the count another one, ah ah ah! */ |
| |
185 count++; |
| |
186 |
| |
187 /* Finally break out of the loop. */ |
| |
188 break; |
| |
189 } |
| |
190 } |
| |
191 |
| |
192 return count; |
| |
193 } |
| |
194 |
| |
195 static GStrv |
| |
196 purple_ircv3_parser_build_params(PurpleIRCv3Parser *parser, |
| |
197 const gchar *middle, const gchar *coda, |
| |
198 const gchar *trailing, guint *n_params) |
| |
199 { |
| |
200 GStrvBuilder *builder = g_strv_builder_new(); |
| |
201 GStrv result = NULL; |
| |
202 |
| |
203 purple_ircv3_parser_extract_params(parser, builder, middle); |
| |
204 |
| |
205 if(*coda != '\0') { |
| |
206 g_strv_builder_add(builder, trailing); |
| |
207 } |
| |
208 |
| |
209 result = g_strv_builder_end(builder); |
| |
210 |
| |
211 g_strv_builder_unref(builder); |
| |
212 |
| |
213 if(result != NULL && n_params != NULL) { |
| |
214 *n_params = g_strv_length(result); |
| |
215 } |
| |
216 |
| |
217 return result; |
| |
218 } |
| |
219 |
| |
220 /****************************************************************************** |
| |
221 * Handlers |
| |
222 *****************************************************************************/ |
| |
223 static gboolean |
| |
224 purple_ircv3_fallback_handler(PurpleIRCv3Message *message, |
| |
225 GError **error, |
| |
226 G_GNUC_UNUSED gpointer data) |
| |
227 { |
| |
228 g_set_error(error, PURPLE_IRCV3_DOMAIN, 0, "no handler for command %s", |
| |
229 purple_ircv3_message_get_command(message)); |
| |
230 |
| |
231 return FALSE; |
| |
232 } |
| |
233 |
| |
234 /****************************************************************************** |
| |
235 * GObject Implementation |
| |
236 *****************************************************************************/ |
| |
237 static void |
| |
238 purple_ircv3_parser_finalize(GObject *obj) { |
| |
239 PurpleIRCv3Parser *parser = PURPLE_IRCV3_PARSER(obj); |
| |
240 |
| |
241 g_clear_pointer(&parser->regex_message, g_regex_unref); |
| |
242 g_clear_pointer(&parser->regex_tags, g_regex_unref); |
| |
243 |
| |
244 g_hash_table_destroy(parser->handlers); |
| |
245 |
| |
246 G_OBJECT_CLASS(purple_ircv3_parser_parent_class)->finalize(obj); |
| |
247 } |
| |
248 |
| |
249 static void |
| |
250 purple_ircv3_parser_init(PurpleIRCv3Parser *parser) { |
| |
251 parser->regex_message = g_regex_new("(?:@(?<tags>[^ ]+) )?" |
| |
252 "(?::(?<source>[^ ]+) +)?" |
| |
253 "(?<command>[^ :]+)" |
| |
254 "(?: +(?<middle>(?:[^ :]+(?: +[^ :]+)*)))*" |
| |
255 "(?<coda> +:(?<trailing>.*)?)?", |
| |
256 0, 0, NULL); |
| |
257 g_assert(parser->regex_message != NULL); |
| |
258 |
| |
259 parser->regex_tags = g_regex_new("(?<key>(?<client_prefix>\\+?)" |
| |
260 "(?:(?<vendor>[A-Za-z0-9-\\.]+)\\/)?" |
| |
261 "(?<key_name>[A-Za-z0-9-]+)" |
| |
262 ")" |
| |
263 "(?:=(?<value>[^\r\n;]*))?(?:;|$)", |
| |
264 0, 0, NULL); |
| |
265 g_assert(parser->regex_tags != NULL); |
| |
266 |
| |
267 parser->fallback_handler = purple_ircv3_fallback_handler; |
| |
268 parser->handlers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, |
| |
269 NULL); |
| |
270 } |
| |
271 |
| |
272 static void |
| |
273 purple_ircv3_parser_class_init(PurpleIRCv3ParserClass *klass) { |
| |
274 GObjectClass *obj_class = G_OBJECT_CLASS(klass); |
| |
275 |
| |
276 obj_class->finalize = purple_ircv3_parser_finalize; |
| |
277 } |
| |
278 |
| |
279 /****************************************************************************** |
| |
280 * Public API |
| |
281 *****************************************************************************/ |
| |
282 PurpleIRCv3Parser * |
| |
283 purple_ircv3_parser_new(void) { |
| |
284 return g_object_new(PURPLE_IRCV3_TYPE_PARSER, NULL); |
| |
285 } |
| |
286 |
| |
287 void |
| |
288 purple_ircv3_parser_set_fallback_handler(PurpleIRCv3Parser *parser, |
| |
289 PurpleIRCv3MessageHandler handler) |
| |
290 { |
| |
291 g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser)); |
| |
292 |
| |
293 parser->fallback_handler = handler; |
| |
294 } |
| |
295 |
| |
296 gboolean |
| |
297 purple_ircv3_parser_parse(PurpleIRCv3Parser *parser, const gchar *buffer, |
| |
298 GError **error, gpointer data) |
| |
299 { |
| |
300 PurpleIRCv3Message *message = NULL; |
| |
301 PurpleIRCv3MessageHandler handler = NULL; |
| |
302 GError *local_error = NULL; |
| |
303 GHashTable *tags = NULL; |
| |
304 GMatchInfo *info = NULL; |
| |
305 GStrv params = NULL; |
| |
306 gchar *coda = NULL; |
| |
307 gchar *command = NULL; |
| |
308 gchar *middle = NULL; |
| |
309 gchar *source = NULL; |
| |
310 gchar *tags_string = NULL; |
| |
311 gchar *trailing = NULL; |
| |
312 gboolean matches = FALSE; |
| |
313 gboolean result = FALSE; |
| |
314 |
| |
315 g_return_val_if_fail(PURPLE_IRCV3_IS_PARSER(parser), FALSE); |
| |
316 g_return_val_if_fail(buffer != NULL, FALSE); |
| |
317 |
| |
318 /* Check if the buffer matches our regex for messages. */ |
| |
319 matches = g_regex_match(parser->regex_message, buffer, 0, &info); |
| |
320 if(!matches) { |
| |
321 g_set_error(error, PURPLE_IRCV3_DOMAIN, 0, |
| |
322 "failed to parser buffer '%s'", buffer); |
| |
323 |
| |
324 g_match_info_unref(info); |
| |
325 |
| |
326 return FALSE; |
| |
327 } |
| |
328 |
| |
329 /* Extract the command from the buffer, so we can find the handler. */ |
| |
330 command = g_match_info_fetch_named(info, "command"); |
| |
331 handler = g_hash_table_lookup(parser->handlers, command); |
| |
332 if(handler == NULL) { |
| |
333 if(parser->fallback_handler == NULL) { |
| |
334 g_set_error(error, PURPLE_IRCV3_DOMAIN, 0, |
| |
335 "no handler found for command %s and no default " |
| |
336 "handler set.", command); |
| |
337 |
| |
338 g_free(command); |
| |
339 g_match_info_unref(info); |
| |
340 |
| |
341 return FALSE; |
| |
342 } |
| |
343 |
| |
344 handler = parser->fallback_handler; |
| |
345 } |
| |
346 |
| |
347 /* If we made it this far, we have our handler, so lets get the rest of the |
| |
348 * parameters and call the handler. |
| |
349 */ |
| |
350 message = purple_ircv3_message_new(command); |
| |
351 |
| |
352 tags_string = g_match_info_fetch_named(info, "tags"); |
| |
353 tags = purple_ircv3_parser_parse_tags(parser, tags_string, &local_error); |
| |
354 g_free(tags_string); |
| |
355 if(local_error != NULL) { |
| |
356 g_propagate_error(error, local_error); |
| |
357 |
| |
358 g_free(command); |
| |
359 g_clear_object(&message); |
| |
360 g_hash_table_destroy(tags); |
| |
361 g_match_info_unref(info); |
| |
362 |
| |
363 return FALSE; |
| |
364 } |
| |
365 if(tags != NULL) { |
| |
366 purple_ircv3_message_set_tags(message, tags); |
| |
367 g_hash_table_unref(tags); |
| |
368 } |
| |
369 |
| |
370 source = g_match_info_fetch_named(info, "source"); |
| |
371 if(!purple_strempty(source)) { |
| |
372 purple_ircv3_message_set_source(message, source); |
| |
373 } |
| |
374 g_free(source); |
| |
375 |
| |
376 middle = g_match_info_fetch_named(info, "middle"); |
| |
377 coda = g_match_info_fetch_named(info, "coda"); |
| |
378 trailing = g_match_info_fetch_named(info, "trailing"); |
| |
379 params = purple_ircv3_parser_build_params(parser, middle, coda, trailing, |
| |
380 NULL); |
| |
381 if(params != NULL) { |
| |
382 purple_ircv3_message_set_params(message, params); |
| |
383 } |
| |
384 |
| |
385 g_free(command); |
| |
386 g_free(middle); |
| |
387 g_free(coda); |
| |
388 g_free(trailing); |
| |
389 g_strfreev(params); |
| |
390 |
| |
391 /* Call the handler. */ |
| |
392 result = handler(message, error, data); |
| |
393 |
| |
394 /* Cleanup the left overs. */ |
| |
395 g_match_info_unref(info); |
| |
396 g_clear_object(&message); |
| |
397 |
| |
398 return result; |
| |
399 } |
| |
400 |
| |
401 void |
| |
402 purple_ircv3_parser_add_handler(PurpleIRCv3Parser *parser, |
| |
403 const char *command, |
| |
404 PurpleIRCv3MessageHandler handler) |
| |
405 { |
| |
406 g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser)); |
| |
407 g_return_if_fail(command != NULL); |
| |
408 g_return_if_fail(handler != NULL); |
| |
409 |
| |
410 g_hash_table_insert(parser->handlers, g_strdup(command), handler); |
| |
411 } |
| |
412 |
| |
413 void |
| |
414 purple_ircv3_parser_add_handlers(PurpleIRCv3Parser *parser, |
| |
415 PurpleIRCv3MessageHandler handler, |
| |
416 ...) |
| |
417 { |
| |
418 va_list vargs; |
| |
419 const char *command = NULL; |
| |
420 |
| |
421 g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser)); |
| |
422 g_return_if_fail(handler != NULL); |
| |
423 |
| |
424 va_start(vargs, handler); |
| |
425 |
| |
426 while((command = va_arg(vargs, const char *)) != NULL) { |
| |
427 purple_ircv3_parser_add_handler(parser, command, handler); |
| |
428 } |
| |
429 |
| |
430 va_end(vargs); |
| |
431 } |
| |
432 |
| |
433 void |
| |
434 purple_ircv3_parser_add_default_handlers(PurpleIRCv3Parser *parser) { |
| |
435 g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser)); |
| |
436 |
| |
437 purple_ircv3_parser_set_fallback_handler(parser, |
| |
438 purple_ircv3_message_handler_fallback); |
| |
439 |
| |
440 /* Core functionality. */ |
| |
441 purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_CAP, |
| |
442 purple_ircv3_capabilities_message_handler); |
| |
443 purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_NOTICE, |
| |
444 purple_ircv3_message_handler_privmsg); |
| |
445 purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_PING, |
| |
446 purple_ircv3_message_handler_ping); |
| |
447 purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_PRIVMSG, |
| |
448 purple_ircv3_message_handler_privmsg); |
| |
449 |
| |
450 /* Topic stuff. */ |
| |
451 purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_TOPIC, |
| |
452 purple_ircv3_message_handler_topic); |
| |
453 purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_NOTOPIC, |
| |
454 purple_ircv3_message_handler_topic); |
| |
455 purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_TOPIC, |
| |
456 purple_ircv3_message_handler_topic); |
| |
457 |
| |
458 /* Post Registration Greetings */ |
| |
459 purple_ircv3_parser_add_handlers(parser, |
| |
460 purple_ircv3_message_handler_status_ignore_param0, |
| |
461 PURPLE_IRCV3_RPL_WELCOME, |
| |
462 PURPLE_IRCV3_RPL_YOURHOST, |
| |
463 PURPLE_IRCV3_RPL_CREATED, |
| |
464 PURPLE_IRCV3_RPL_MYINFO, |
| |
465 NULL); |
| |
466 |
| |
467 /* Luser's */ |
| |
468 purple_ircv3_parser_add_handlers(parser, |
| |
469 purple_ircv3_message_handler_status_ignore_param0, |
| |
470 PURPLE_IRCV3_RPL_LUSERCLIENT, |
| |
471 PURPLE_IRCV3_RPL_LUSEROP, |
| |
472 PURPLE_IRCV3_RPL_LUSERUNKNOWN, |
| |
473 PURPLE_IRCV3_RPL_LUSERCHANNELS, |
| |
474 PURPLE_IRCV3_RPL_LUSERME, |
| |
475 NULL); |
| |
476 |
| |
477 /* MOTD */ |
| |
478 purple_ircv3_parser_add_handlers(parser, |
| |
479 purple_ircv3_message_handler_status_ignore_param0, |
| |
480 PURPLE_IRCV3_RPL_MOTD, |
| |
481 PURPLE_IRCV3_RPL_MOTDSTART, |
| |
482 PURPLE_IRCV3_RPL_ENDOFMOTD, |
| |
483 NULL); |
| |
484 |
| |
485 /* SASL stuff. */ |
| |
486 purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_LOGGEDIN, |
| |
487 purple_ircv3_sasl_logged_in); |
| |
488 purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_LOGGEDOUT, |
| |
489 purple_ircv3_sasl_logged_out); |
| |
490 purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_NICKLOCKED, |
| |
491 purple_ircv3_sasl_nick_locked); |
| |
492 purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_SASLSUCCESS, |
| |
493 purple_ircv3_sasl_success); |
| |
494 purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_SASLFAIL, |
| |
495 purple_ircv3_sasl_failed); |
| |
496 purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_SASLTOOLONG, |
| |
497 purple_ircv3_sasl_message_too_long); |
| |
498 purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_SASLABORTED, |
| |
499 purple_ircv3_sasl_aborted); |
| |
500 purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_SASLALREADY, |
| |
501 purple_ircv3_sasl_already_authed); |
| |
502 purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_SASLMECHS, |
| |
503 purple_ircv3_sasl_mechanisms); |
| |
504 purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_AUTHENTICATE, |
| |
505 purple_ircv3_sasl_authenticate); |
| |
506 |
| |
507 } |