summaryrefslogtreecommitdiff
path: root/src/irc/core/irc-cap.c
blob: 1a60d99b792458ea4700a380e51ef0f1126bb3dc (plain)
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
/*  irc-cap.c : irssi

    Copyright (C) 2015 The Lemon Man

    This program 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; either version 2 of the License, or
    (at your option) any later version.

    This program 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.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

#include "module.h"
#include "signals.h"
#include "misc.h"

#include "irc-cap.h"
#include "irc-servers.h"

int cap_toggle (IRC_SERVER_REC *server, char *cap, int enable)
{
	if (cap == NULL || *cap == '\0')
		return FALSE;

	/* If the negotiation hasn't been completed yet just queue the requests */
	if (!server->cap_complete) {
		if (enable && !gslist_find_string(server->cap_queue, cap)) {
			server->cap_queue = g_slist_prepend(server->cap_queue, g_strdup(cap));
			return TRUE;
		}
		else if (!enable && gslist_find_string(server->cap_queue, cap)) {
			server->cap_queue = gslist_delete_string(server->cap_queue, cap, g_free);
			return TRUE;
		}

		return FALSE;
	}

	if (enable && !gslist_find_string(server->cap_active, cap)) {
		/* Make sure the required cap is supported by the server */
		if (!g_hash_table_lookup_extended(server->cap_supported, cap, NULL, NULL))
			return FALSE;

		irc_send_cmdv(server, "CAP REQ %s", cap);
		return TRUE;
	}
	else if (!enable && gslist_find_string(server->cap_active, cap)) {
		irc_send_cmdv(server, "CAP REQ -%s", cap);
		return TRUE;
	}

	return FALSE;
}

void cap_finish_negotiation (IRC_SERVER_REC *server)
{
	if (server->cap_complete)
		return;

	server->cap_complete = TRUE;
	irc_send_cmd_now(server, "CAP END");

	signal_emit("server cap end", 1, server);
}

static void cap_emit_signal (IRC_SERVER_REC *server, char *cmd, char *args)
{
	char *signal_name;

	signal_name = g_strdup_printf("server cap %s %s", cmd, args? args: "");
	signal_emit(signal_name, 1, server);
	g_free(signal_name);
}

static gboolean parse_cap_name(char *name, char **key, char **val)
{
	const char *eq;

	g_return_val_if_fail(name != NULL, FALSE);
	g_return_val_if_fail(name[0] != '\0', FALSE);

	eq = strchr(name, '=');
	/* KEY only value */
	if (eq == NULL) {
		*key = g_strdup(name);
		*val = NULL;
	/* Some values are in a KEY=VALUE form, parse them */
	} else {
		*key = g_strndup(name, (gsize)(eq - name));
		*val = g_strdup(eq + 1);
	}

	return TRUE;
}

static void event_cap (IRC_SERVER_REC *server, char *args, char *nick, char *address)
{
	GSList *tmp;
	GString *cmd;
	char *params, *evt, *list, *star, **caps;
	int i, caps_length, disable, avail_caps, multiline;

	params = event_get_params(args, 4, NULL, &evt, &star, &list);
	if (params == NULL)
		return;

	/* Multiline responses have an additional parameter and we have to do
	 * this stupid dance to parse them */
	if (!g_ascii_strcasecmp(evt, "LS") && !strcmp(star, "*")) {
		multiline = TRUE;
	}
	/* This branch covers the '*' parameter isn't present, adjust the
	 * parameter pointer to compensate for this */
	else if (list[0] == '\0') {
		multiline = FALSE;
		list = star;
	}
	/* Malformed request, terminate the negotiation */
	else {
		cap_finish_negotiation(server);
		g_warn_if_reached();
		return;
	}

	/* The table is created only when needed */
	if (server->cap_supported == NULL) {
		server->cap_supported = g_hash_table_new_full(g_str_hash,
							      g_str_equal,
							      g_free, g_free);
	}

	/* Strip the trailing whitespaces before splitting the string, some servers send responses with
	 * superfluous whitespaces that g_strsplit the interprets as tokens */
	caps = g_strsplit(g_strchomp(list), " ", -1);
	caps_length = g_strv_length(caps);

	if (!g_ascii_strcasecmp(evt, "LS")) {
		if (!server->cap_in_multiline) {
			/* Throw away everything and start from scratch */
			g_hash_table_remove_all(server->cap_supported);
		}

		server->cap_in_multiline = multiline;

		/* Create a list of the supported caps */
		for (i = 0; i < caps_length; i++) {
			char *key, *val;

			if (!parse_cap_name(caps[i], &key, &val)) {
				g_warning("Invalid CAP %s key/value pair", evt);
				continue;
			}

			if (g_hash_table_lookup_extended(server->cap_supported, key, NULL, NULL)) {
				/* The specification doesn't say anything about
				 * duplicated values, let's just warn the user */
				g_warning("The server sent the %s capability twice", key);
			}
			g_hash_table_insert(server->cap_supported, key, val);
		}

		/* A multiline response is always terminated by a normal one,
		 * wait until we receive that one to require any CAP */
		if (multiline == FALSE) {
			/* No CAP has been requested */
			if (server->cap_queue == NULL) {
				cap_finish_negotiation(server);
			}
			else {
				cmd = g_string_new("CAP REQ :");

				avail_caps = 0;

				/* Check whether the cap is supported by the server */
				for (tmp = server->cap_queue; tmp != NULL; tmp = tmp->next) {
					if (g_hash_table_lookup_extended(server->cap_supported, tmp->data, NULL, NULL)) {
						if (avail_caps > 0)
							g_string_append_c(cmd, ' ');
						g_string_append(cmd, tmp->data);

						avail_caps++;
					}
				}

				/* Clear the queue here */
				gslist_free_full(server->cap_queue, (GDestroyNotify) g_free);
				server->cap_queue = NULL;

				/* If the server doesn't support any cap we requested close the negotiation here */
				if (avail_caps > 0)
					irc_send_cmd_now(server, cmd->str);
				else
					cap_finish_negotiation(server);

				g_string_free(cmd, TRUE);
			}
		}
	}
	else if (!g_ascii_strcasecmp(evt, "ACK")) {
		int got_sasl = FALSE;

		/* Emit a signal for every ack'd cap */
		for (i = 0; i < caps_length; i++) {
			disable = (*caps[i] == '-');

			if (disable)
				server->cap_active = gslist_delete_string(server->cap_active, caps[i] + 1, g_free);
			else
				server->cap_active = g_slist_prepend(server->cap_active, g_strdup(caps[i]));

			if (!strcmp(caps[i], "sasl"))
				got_sasl = TRUE;

			cap_emit_signal(server, "ack", caps[i]);
		}

		/* Hopefully the server has ack'd all the caps requested and we're ready to terminate the
		 * negotiation, unless sasl was requested. In this case we must not terminate the negotiation
		 * until the sasl handshake is over. */
		if (got_sasl == FALSE)
			cap_finish_negotiation(server);
	}
	else if (!g_ascii_strcasecmp(evt, "NAK")) {
		g_warning("The server answered with a NAK to our CAP request, this should not happen");

		/* A NAK'd request means that a required cap can't be enabled or disabled, don't update the
		 * list of active caps and notify the listeners. */
		for (i = 0; i < caps_length; i++)
			cap_emit_signal(server, "nak", caps[i]);
	}
	else if (!g_ascii_strcasecmp(evt, "NEW")) {
		for (i = 0; i < caps_length; i++) {
			char *key, *val;

			if (!parse_cap_name(caps[i], &key, &val)) {
				g_warning("Invalid CAP %s key/value pair", evt);
				continue;
			}

			g_hash_table_insert(server->cap_supported, key, val);
			cap_emit_signal(server, "new", key);
		}
	}
	else if (!g_ascii_strcasecmp(evt, "DEL")) {
		for (i = 0; i < caps_length; i++) {
			char *key, *val;

			if (!parse_cap_name(caps[i], &key, &val)) {
				g_warning("Invalid CAP %s key/value pair", evt);
				continue;
			}

			g_hash_table_remove(server->cap_supported, key);
			cap_emit_signal(server, "delete", key);
			/* The server removed this CAP, remove it from the list
			 * of the active ones if we had requested it */
			server->cap_active = gslist_delete_string(server->cap_active, key, g_free);
			/* We don't transfer the ownership of those two
			 * variables this time, just free them when we're done. */
			g_free(key);
			g_free(val);
		}
	}
	else {
		g_warning("Unhandled CAP subcommand %s", evt);
	}

	g_strfreev(caps);
	g_free(params);
}

static void event_invalid_cap (IRC_SERVER_REC *server, const char *data, const char *from)
{
	/* The server didn't understand one (or more) requested caps, terminate the negotiation.
	 * This could be handled in a graceful way but since it shouldn't really ever happen this seems a
	 * good way to deal with 410 errors. */
	server->cap_complete = FALSE;
	irc_send_cmd_now(server, "CAP END");
}

void cap_init (void)
{
	signal_add_first("event cap", (SIGNAL_FUNC) event_cap);
	signal_add_first("event 410", (SIGNAL_FUNC) event_invalid_cap);
}

void cap_deinit (void)
{
	signal_remove("event cap", (SIGNAL_FUNC) event_cap);
	signal_remove("event 410", (SIGNAL_FUNC) event_invalid_cap);
}