Kill off unneeded GLIB_CHECK_VERSION checks in libpurple. Refs #10024.
[pidgin-git.git] / libpurple / protocols / msn / nexus.c
blob2ffcac9f5341272496ddb3842cfaaec1007c307d
1 /**
2 * @file nexus.c MSN Nexus functions
4 * purple
6 * Purple is the legal property of its developers, whose names are too numerous
7 * to list here. Please refer to the COPYRIGHT file distributed with this
8 * source distribution.
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
20 * You should have received a copy of the GNU General Public License
21 * along with this program; if not, write to the Free Software
22 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
24 #include "msn.h"
25 #include "soap.h"
26 #include "nexus.h"
27 #include "notification.h"
29 /**************************************************************************
30 * Valid Ticket Tokens
31 **************************************************************************/
33 #define SSO_VALID_TICKET_DOMAIN 0
34 #define SSO_VALID_TICKET_POLICY 1
35 static char *ticket_domains[][2] = {
36 /* http://msnpiki.msnfanatic.com/index.php/MSNP15:SSO */
37 /* {"Domain", "Policy Ref URI"}, Purpose */
38 {"messengerclear.live.com", NULL}, /* Authentication for messenger. */
39 {"messenger.msn.com", "?id=507"}, /* Authentication for receiving OIMs. */
40 {"contacts.msn.com", "MBI"}, /* Authentication for the Contact server. */
41 {"messengersecure.live.com", "MBI_SSL"}, /* Authentication for sending OIMs. */
42 {"spaces.live.com", "MBI"}, /* Authentication for the Windows Live Spaces */
43 {"livecontacts.live.com", "MBI"}, /* Live Contacts API, a simplified version of the Contacts SOAP service */
44 {"storage.live.com", "MBI"}, /* Storage REST API */
47 /**************************************************************************
48 * Main
49 **************************************************************************/
51 MsnNexus *
52 msn_nexus_new(MsnSession *session)
54 MsnNexus *nexus;
55 int i;
57 nexus = g_new0(MsnNexus, 1);
58 nexus->session = session;
60 nexus->token_len = sizeof(ticket_domains) / sizeof(char *[2]);
61 nexus->tokens = g_new0(MsnTicketToken, nexus->token_len);
63 for (i = 0; i < nexus->token_len; i++)
64 nexus->tokens[i].token = g_hash_table_new_full(g_str_hash, g_str_equal,
65 g_free, g_free);
67 return nexus;
70 void
71 msn_nexus_destroy(MsnNexus *nexus)
73 int i;
74 for (i = 0; i < nexus->token_len; i++) {
75 g_hash_table_destroy(nexus->tokens[i].token);
76 g_free(nexus->tokens[i].secret);
77 g_slist_free(nexus->tokens[i].updates);
80 g_free(nexus->tokens);
81 g_free(nexus->policy);
82 g_free(nexus->nonce);
83 g_free(nexus->cipher);
84 g_free(nexus->secret);
85 g_free(nexus);
88 /**************************************************************************
89 * RPS/SSO Authentication
90 **************************************************************************/
92 static char *
93 rps_create_key(const char *key, int key_len, const char *data, size_t data_len)
95 const guchar magic[] = "WS-SecureConversation";
96 const int magic_len = sizeof(magic) - 1;
98 PurpleCipherContext *hmac;
99 guchar hash1[20], hash2[20], hash3[20], hash4[20];
100 char *result;
102 hmac = purple_cipher_context_new_by_name("hmac", NULL);
104 purple_cipher_context_set_option(hmac, "hash", "sha1");
105 purple_cipher_context_set_key_with_len(hmac, (guchar *)key, key_len);
106 purple_cipher_context_append(hmac, magic, magic_len);
107 purple_cipher_context_append(hmac, (guchar *)data, data_len);
108 purple_cipher_context_digest(hmac, sizeof(hash1), hash1, NULL);
110 purple_cipher_context_reset(hmac, NULL);
111 purple_cipher_context_set_option(hmac, "hash", "sha1");
112 purple_cipher_context_set_key_with_len(hmac, (guchar *)key, key_len);
113 purple_cipher_context_append(hmac, hash1, 20);
114 purple_cipher_context_append(hmac, magic, magic_len);
115 purple_cipher_context_append(hmac, (guchar *)data, data_len);
116 purple_cipher_context_digest(hmac, sizeof(hash2), hash2, NULL);
118 purple_cipher_context_reset(hmac, NULL);
119 purple_cipher_context_set_option(hmac, "hash", "sha1");
120 purple_cipher_context_set_key_with_len(hmac, (guchar *)key, key_len);
121 purple_cipher_context_append(hmac, hash1, 20);
122 purple_cipher_context_digest(hmac, sizeof(hash3), hash3, NULL);
124 purple_cipher_context_reset(hmac, NULL);
125 purple_cipher_context_set_option(hmac, "hash", "sha1");
126 purple_cipher_context_set_key_with_len(hmac, (guchar *)key, key_len);
127 purple_cipher_context_append(hmac, hash3, sizeof(hash3));
128 purple_cipher_context_append(hmac, magic, magic_len);
129 purple_cipher_context_append(hmac, (guchar *)data, data_len);
130 purple_cipher_context_digest(hmac, sizeof(hash4), hash4, NULL);
132 purple_cipher_context_destroy(hmac);
134 result = g_malloc(24);
135 memcpy(result, hash2, sizeof(hash2));
136 memcpy(result + sizeof(hash2), hash4, 4);
138 return result;
141 static char *
142 des3_cbc(const char *key, const char *iv, const char *data, int len, gboolean decrypt)
144 PurpleCipherContext *des3;
145 char *out;
146 size_t outlen;
148 des3 = purple_cipher_context_new_by_name("des3", NULL);
149 purple_cipher_context_set_key(des3, (guchar *)key);
150 purple_cipher_context_set_batch_mode(des3, PURPLE_CIPHER_BATCH_MODE_CBC);
151 purple_cipher_context_set_iv(des3, (guchar *)iv, 8);
153 out = g_malloc(len);
154 if (decrypt)
155 purple_cipher_context_decrypt(des3, (guchar *)data, len, (guchar *)out, &outlen);
156 else
157 purple_cipher_context_encrypt(des3, (guchar *)data, len, (guchar *)out, &outlen);
159 purple_cipher_context_destroy(des3);
161 return out;
164 #define CRYPT_MODE_CBC 1
165 #define CIPHER_TRIPLE_DES 0x6603
166 #define HASH_SHA1 0x8004
167 static char *
168 msn_rps_encrypt(MsnNexus *nexus)
170 MsnUsrKey *usr_key;
171 const char magic1[] = "SESSION KEY HASH";
172 const char magic2[] = "SESSION KEY ENCRYPTION";
173 PurpleCipherContext *hmac;
174 size_t len;
175 guchar hash[20];
176 char *key1, *key2, *key3;
177 gsize key1_len;
178 int *iv;
179 char *nonce_fixed;
180 char *cipher;
181 char *response;
183 usr_key = g_malloc(sizeof(MsnUsrKey));
184 usr_key->size = GUINT32_TO_LE(28);
185 usr_key->crypt_mode = GUINT32_TO_LE(CRYPT_MODE_CBC);
186 usr_key->cipher_type = GUINT32_TO_LE(CIPHER_TRIPLE_DES);
187 usr_key->hash_type = GUINT32_TO_LE(HASH_SHA1);
188 usr_key->iv_len = GUINT32_TO_LE(8);
189 usr_key->hash_len = GUINT32_TO_LE(20);
190 usr_key->cipher_len = GUINT32_TO_LE(72);
192 key1 = (char *)purple_base64_decode((const char *)nexus->tokens[MSN_AUTH_MESSENGER].secret, &key1_len);
193 key2 = rps_create_key(key1, key1_len, magic1, sizeof(magic1) - 1);
194 key3 = rps_create_key(key1, key1_len, magic2, sizeof(magic2) - 1);
196 iv = (int *)usr_key->iv;
197 iv[0] = rand();
198 iv[1] = rand();
200 len = strlen(nexus->nonce);
201 hmac = purple_cipher_context_new_by_name("hmac", NULL);
202 purple_cipher_context_set_option(hmac, "hash", "sha1");
203 purple_cipher_context_set_key_with_len(hmac, (guchar *)key2, 24);
204 purple_cipher_context_append(hmac, (guchar *)nexus->nonce, len);
205 purple_cipher_context_digest(hmac, 20, hash, NULL);
206 purple_cipher_context_destroy(hmac);
208 /* We need to pad this to 72 bytes, apparently */
209 nonce_fixed = g_malloc(len + 8);
210 memcpy(nonce_fixed, nexus->nonce, len);
211 memset(nonce_fixed + len, 0x08, 8);
212 cipher = des3_cbc(key3, usr_key->iv, nonce_fixed, len + 8, FALSE);
213 g_free(nonce_fixed);
215 memcpy(usr_key->hash, hash, 20);
216 memcpy(usr_key->cipher, cipher, 72);
218 g_free(key1);
219 g_free(key2);
220 g_free(key3);
221 g_free(cipher);
223 response = purple_base64_encode((guchar *)usr_key, sizeof(MsnUsrKey));
225 g_free(usr_key);
227 return response;
230 /**************************************************************************
231 * Login
232 **************************************************************************/
234 /* Used to specify which token to update when only doing single updates */
235 typedef struct _MsnNexusUpdateData MsnNexusUpdateData;
236 struct _MsnNexusUpdateData {
237 MsnNexus *nexus;
238 int id;
241 typedef struct _MsnNexusUpdateCallback MsnNexusUpdateCallback;
242 struct _MsnNexusUpdateCallback {
243 GSourceFunc cb;
244 gpointer data;
247 static gboolean
248 nexus_parse_token(MsnNexus *nexus, int id, xmlnode *node)
250 char *token_str, *expiry_str;
251 const char *id_str;
252 char **elems, **cur, **tokens;
253 xmlnode *token = xmlnode_get_child(node, "RequestedSecurityToken/BinarySecurityToken");
254 xmlnode *secret = xmlnode_get_child(node, "RequestedProofToken/BinarySecret");
255 xmlnode *expires = xmlnode_get_child(node, "LifeTime/Expires");
257 if (!token)
258 return FALSE;
260 /* Use the ID that the server sent us */
261 if (id == -1) {
262 id_str = xmlnode_get_attrib(token, "Id");
263 if (id_str == NULL)
264 return FALSE;
266 id = atol(id_str + 7) - 1; /* 'Compact#' or 'PPToken#' */
267 if (id >= nexus->token_len)
268 return FALSE; /* Where did this come from? */
271 token_str = xmlnode_get_data(token);
272 if (token_str == NULL)
273 return FALSE;
275 g_hash_table_remove_all(nexus->tokens[id].token);
277 elems = g_strsplit(token_str, "&", 0);
279 for (cur = elems; *cur != NULL; cur++) {
280 tokens = g_strsplit(*cur, "=", 2);
281 g_hash_table_insert(nexus->tokens[id].token, tokens[0], tokens[1]);
282 /* Don't free each of the tokens, only the array. */
283 g_free(tokens);
285 g_strfreev(elems);
286 g_free(token_str);
288 if (secret)
289 nexus->tokens[id].secret = xmlnode_get_data(secret);
290 else
291 nexus->tokens[id].secret = NULL;
293 /* Yay for MS using ISO-8601 */
294 expiry_str = xmlnode_get_data(expires);
295 nexus->tokens[id].expiry = purple_str_to_time(expiry_str,
296 FALSE, NULL, NULL, NULL);
297 g_free(expiry_str);
299 purple_debug_info("msn", "Updated ticket for domain '%s', expires at %" G_GINT64_FORMAT ".\n",
300 ticket_domains[id][SSO_VALID_TICKET_DOMAIN],
301 (gint64)nexus->tokens[id].expiry);
302 return TRUE;
305 static gboolean
306 nexus_parse_collection(MsnNexus *nexus, int id, xmlnode *collection)
308 xmlnode *node;
309 gboolean result;
311 node = xmlnode_get_child(collection, "RequestSecurityTokenResponse");
313 if (!node)
314 return FALSE;
316 result = TRUE;
317 for (; node && result; node = node->next) {
318 xmlnode *endpoint = xmlnode_get_child(node, "AppliesTo/EndpointReference/Address");
319 char *address = xmlnode_get_data(endpoint);
321 if (g_str_equal(address, "http://Passport.NET/tb")) {
322 /* This node contains the stuff for updating tokens. */
323 char *data;
324 xmlnode *cipher = xmlnode_get_child(node, "RequestedSecurityToken/EncryptedData/CipherData/CipherValue");
325 xmlnode *secret = xmlnode_get_child(node, "RequestedProofToken/BinarySecret");
327 nexus->cipher = xmlnode_get_data(cipher);
328 data = xmlnode_get_data(secret);
329 nexus->secret = (char *)purple_base64_decode(data, NULL);
330 g_free(data);
332 } else {
333 result = nexus_parse_token(nexus, id, node);
335 g_free(address);
338 return result;
341 static void
342 nexus_got_response_cb(MsnSoapMessage *req, MsnSoapMessage *resp, gpointer data)
344 MsnNexus *nexus = data;
345 MsnSession *session = nexus->session;
346 const char *ticket;
347 char *response;
349 if (resp == NULL) {
350 msn_session_set_error(session, MSN_ERROR_SERVCONN, _("Windows Live ID authentication:Unable to connect"));
351 return;
354 if (!nexus_parse_collection(nexus, -1,
355 xmlnode_get_child(resp->xml,
356 "Body/RequestSecurityTokenResponseCollection"))) {
357 msn_session_set_error(session, MSN_ERROR_SERVCONN, _("Windows Live ID authentication:Invalid response"));
358 return;
361 ticket = msn_nexus_get_token_str(nexus, MSN_AUTH_MESSENGER);
362 response = msn_rps_encrypt(nexus);
363 msn_got_login_params(session, ticket, response);
364 g_free(response);
367 /*when connect, do the SOAP Style windows Live ID authentication */
368 void
369 msn_nexus_connect(MsnNexus *nexus)
371 MsnSession *session = nexus->session;
372 const char *username;
373 const char *password;
374 char *password_xml;
375 GString *domains;
376 char *request;
377 int i;
379 MsnSoapMessage *soap;
381 purple_debug_info("msn", "Starting Windows Live ID authentication\n");
382 msn_session_set_login_step(session, MSN_LOGIN_STEP_GET_COOKIE);
384 username = purple_account_get_username(session->account);
385 password = purple_connection_get_password(session->account->gc);
386 password_xml = g_markup_escape_text(password, MIN(strlen(password), 16));
388 purple_debug_info("msn", "Logging on %s, with policy '%s', nonce '%s'\n",
389 username, nexus->policy, nexus->nonce);
391 domains = g_string_new(NULL);
392 for (i = 0; i < nexus->token_len; i++) {
393 g_string_append_printf(domains, MSN_SSO_RST_TEMPLATE,
394 i+1,
395 ticket_domains[i][SSO_VALID_TICKET_DOMAIN],
396 ticket_domains[i][SSO_VALID_TICKET_POLICY] != NULL ?
397 ticket_domains[i][SSO_VALID_TICKET_POLICY] :
398 nexus->policy);
401 request = g_strdup_printf(MSN_SSO_TEMPLATE, username, password_xml, domains->str);
402 g_free(password_xml);
403 g_string_free(domains, TRUE);
405 soap = msn_soap_message_new(NULL, xmlnode_from_str(request, -1));
406 g_free(request);
407 msn_soap_message_send(session, soap, MSN_SSO_SERVER, SSO_POST_URL, TRUE,
408 nexus_got_response_cb, nexus);
411 static void
412 nexus_got_update_cb(MsnSoapMessage *req, MsnSoapMessage *resp, gpointer data)
414 MsnNexusUpdateData *ud = data;
415 MsnNexus *nexus = ud->nexus;
416 char iv[8] = {0,0,0,0,0,0,0,0};
417 xmlnode *enckey;
418 char *tmp;
419 char *nonce;
420 gsize len;
421 char *key;
422 GSList *updates;
424 #if 0
425 char *decrypted_pp;
426 #endif
427 char *decrypted_data;
429 if (resp == NULL)
430 return;
432 purple_debug_info("msn", "Got Update Response for %s.\n", ticket_domains[ud->id][SSO_VALID_TICKET_DOMAIN]);
434 enckey = xmlnode_get_child(resp->xml, "Header/Security/DerivedKeyToken");
435 while (enckey) {
436 if (g_str_equal(xmlnode_get_attrib(enckey, "Id"), "EncKey"))
437 break;
438 enckey = xmlnode_get_next_twin(enckey);
440 if (!enckey) {
441 purple_debug_error("msn", "Invalid response in token update.\n");
442 return;
445 tmp = xmlnode_get_data(xmlnode_get_child(enckey, "Nonce"));
446 nonce = (char *)purple_base64_decode(tmp, &len);
447 key = rps_create_key(nexus->secret, 24, nonce, len);
448 g_free(tmp);
449 g_free(nonce);
451 #if 0
452 /* Don't know what this is for yet */
453 tmp = xmlnode_get_data(xmlnode_get_child(resp->xml,
454 "Header/EncryptedPP/EncryptedData/CipherData/CipherValue"));
455 if (tmp) {
456 decrypted_pp = des3_cbc(key, iv, tmp, len, TRUE);
457 g_free(tmp);
458 purple_debug_info("msn", "Got Response Header EncryptedPP: %s\n", decrypted_pp);
459 g_free(decrypted_pp);
461 #endif
463 tmp = xmlnode_get_data(xmlnode_get_child(resp->xml,
464 "Body/EncryptedData/CipherData/CipherValue"));
465 if (tmp) {
466 char *unescaped;
467 xmlnode *rstresponse;
469 unescaped = (char *)purple_base64_decode(tmp, &len);
470 g_free(tmp);
472 decrypted_data = des3_cbc(key, iv, unescaped, len, TRUE);
473 g_free(unescaped);
474 purple_debug_info("msn", "Got Response Body EncryptedData: %s\n", decrypted_data);
476 rstresponse = xmlnode_from_str(decrypted_data, -1);
477 if (g_str_equal(rstresponse->name, "RequestSecurityTokenResponse"))
478 nexus_parse_token(nexus, ud->id, rstresponse);
479 else
480 nexus_parse_collection(nexus, ud->id, rstresponse);
481 g_free(decrypted_data);
484 updates = nexus->tokens[ud->id].updates;
485 nexus->tokens[ud->id].updates = NULL;
486 while (updates != NULL) {
487 MsnNexusUpdateCallback *update = updates->data;
488 if (update->cb)
489 purple_timeout_add(0, update->cb, update->data);
490 g_free(update);
491 updates = g_slist_delete_link(updates, updates);
494 g_free(ud);
497 void
498 msn_nexus_update_token(MsnNexus *nexus, int id, GSourceFunc cb, gpointer data)
500 MsnSession *session = nexus->session;
501 MsnNexusUpdateData *ud;
502 MsnNexusUpdateCallback *update;
503 PurpleCipherContext *sha1;
504 PurpleCipherContext *hmac;
506 char *key;
508 guchar digest[20];
510 struct tm *tm;
511 time_t now;
512 char *now_str;
513 char *timestamp;
514 char *timestamp_b64;
516 char *domain;
517 char *domain_b64;
519 char *signedinfo;
520 gint32 nonce[6];
521 int i;
522 char *nonce_b64;
523 char *signature_b64;
524 guchar signature[20];
526 char *request;
527 MsnSoapMessage *soap;
529 update = g_new0(MsnNexusUpdateCallback, 1);
530 update->cb = cb;
531 update->data = data;
533 if (nexus->tokens[id].updates != NULL) {
534 /* Update already in progress. Just add to list and return. */
535 purple_debug_info("msn",
536 "Ticket update for user '%s' on domain '%s' in progress. Adding request to queue.\n",
537 purple_account_get_username(session->account),
538 ticket_domains[id][SSO_VALID_TICKET_DOMAIN]);
539 nexus->tokens[id].updates = g_slist_prepend(nexus->tokens[id].updates,
540 update);
541 return;
542 } else {
543 purple_debug_info("msn",
544 "Updating ticket for user '%s' on domain '%s'\n",
545 purple_account_get_username(session->account),
546 ticket_domains[id][SSO_VALID_TICKET_DOMAIN]);
547 nexus->tokens[id].updates = g_slist_prepend(nexus->tokens[id].updates,
548 update);
551 ud = g_new0(MsnNexusUpdateData, 1);
552 ud->nexus = nexus;
553 ud->id = id;
555 sha1 = purple_cipher_context_new_by_name("sha1", NULL);
557 domain = g_strdup_printf(MSN_SSO_RST_TEMPLATE,
559 ticket_domains[id][SSO_VALID_TICKET_DOMAIN],
560 ticket_domains[id][SSO_VALID_TICKET_POLICY] != NULL ?
561 ticket_domains[id][SSO_VALID_TICKET_POLICY] :
562 nexus->policy);
563 purple_cipher_context_append(sha1, (guchar *)domain, strlen(domain));
564 purple_cipher_context_digest(sha1, 20, digest, NULL);
565 domain_b64 = purple_base64_encode(digest, 20);
567 now = time(NULL);
568 tm = gmtime(&now);
569 now_str = g_strdup(purple_utf8_strftime("%Y-%m-%dT%H:%M:%SZ", tm));
570 now += 5*60;
571 tm = gmtime(&now);
572 timestamp = g_strdup_printf(MSN_SSO_TIMESTAMP_TEMPLATE,
573 now_str,
574 purple_utf8_strftime("%Y-%m-%dT%H:%M:%SZ", tm));
575 purple_cipher_context_reset(sha1, NULL);
576 purple_cipher_context_append(sha1, (guchar *)timestamp, strlen(timestamp));
577 purple_cipher_context_digest(sha1, 20, digest, NULL);
578 timestamp_b64 = purple_base64_encode(digest, 20);
579 g_free(now_str);
581 purple_cipher_context_destroy(sha1);
583 signedinfo = g_strdup_printf(MSN_SSO_SIGNEDINFO_TEMPLATE,
585 domain_b64,
586 timestamp_b64);
588 for (i = 0; i < 6; i++)
589 nonce[i] = rand();
590 nonce_b64 = purple_base64_encode((guchar *)&nonce, sizeof(nonce));
592 key = rps_create_key(nexus->secret, 24, (char *)nonce, sizeof(nonce));
593 hmac = purple_cipher_context_new_by_name("hmac", NULL);
594 purple_cipher_context_set_option(hmac, "hash", "sha1");
595 purple_cipher_context_set_key_with_len(hmac, (guchar *)key, 24);
596 purple_cipher_context_append(hmac, (guchar *)signedinfo, strlen(signedinfo));
597 purple_cipher_context_digest(hmac, 20, signature, NULL);
598 purple_cipher_context_destroy(hmac);
599 signature_b64 = purple_base64_encode(signature, 20);
601 request = g_strdup_printf(MSN_SSO_TOKEN_UPDATE_TEMPLATE,
602 nexus->cipher,
603 nonce_b64,
604 timestamp,
605 signedinfo,
606 signature_b64,
607 domain);
609 g_free(nonce_b64);
610 g_free(domain_b64);
611 g_free(timestamp_b64);
612 g_free(timestamp);
613 g_free(key);
614 g_free(signature_b64);
615 g_free(signedinfo);
616 g_free(domain);
618 soap = msn_soap_message_new(NULL, xmlnode_from_str(request, -1));
619 g_free(request);
620 msn_soap_message_send(session, soap, MSN_SSO_SERVER, SSO_POST_URL, TRUE,
621 nexus_got_update_cb, ud);
624 GHashTable *
625 msn_nexus_get_token(MsnNexus *nexus, MsnAuthDomains id)
627 g_return_val_if_fail(nexus != NULL, NULL);
628 g_return_val_if_fail(id < nexus->token_len, NULL);
630 return nexus->tokens[id].token;
633 const char *
634 msn_nexus_get_token_str(MsnNexus *nexus, MsnAuthDomains id)
636 static char buf[1024];
637 GHashTable *token = msn_nexus_get_token(nexus, id);
638 const char *msn_t;
639 const char *msn_p;
640 gint ret;
642 g_return_val_if_fail(token != NULL, NULL);
644 msn_t = g_hash_table_lookup(token, "t");
645 msn_p = g_hash_table_lookup(token, "p");
647 g_return_val_if_fail(msn_t != NULL, NULL);
648 g_return_val_if_fail(msn_p != NULL, NULL);
650 ret = g_snprintf(buf, sizeof(buf) - 1, "t=%s&p=%s", msn_t, msn_p);
651 g_return_val_if_fail(ret != -1, NULL);
653 return buf;