7 * Copyright (C) 2005 Thomas Butter <butter@uni-mannheim.de>
9 * This program 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.
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.
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
28 #include <arpa/nameser.h>
30 #ifdef HAVE_ARPA_NAMESER_COMPAT_H
31 #include <arpa/nameser_compat.h>
41 /* Missing from the mingw headers */
43 # define DNS_TYPE_SRV 33
46 # define DNS_TYPE_TXT 16
52 #include "eventloop.h"
61 static DNS_STATUS (WINAPI
*MyDnsQuery_UTF8
) (
62 PCSTR lpstrName
, WORD wType
, DWORD fOptions
,
63 PIP4_ARRAY aipServers
, PDNS_RECORD
* ppQueryResultsSet
,
64 PVOID
* pReserved
) = NULL
;
65 static void (WINAPI
*MyDnsRecordListFree
) (PDNS_RECORD pRecordList
,
66 DNS_FREE_TYPE FreeType
) = NULL
;
69 struct _PurpleTxtResponse
{
73 struct _PurpleSrvQueryData
{
75 PurpleSrvCallback srv
;
76 PurpleTxtCallback txt
;
93 typedef struct _PurpleSrvInternalQuery
{
96 } PurpleSrvInternalQuery
;
98 typedef struct _PurpleSrvResponseContainer
{
99 PurpleSrvResponse
*response
;
101 } PurpleSrvResponseContainer
;
104 * Sort by priority, then by weight. Strictly numerically--no
105 * randomness. Technically we only need to sort by pref and then
106 * make sure any records with weight 0 are at the beginning of
107 * their group, but it's just as easy to sort by weight.
110 responsecompare(gconstpointer ar
, gconstpointer br
)
112 PurpleSrvResponse
*a
= (PurpleSrvResponse
*)ar
;
113 PurpleSrvResponse
*b
= (PurpleSrvResponse
*)br
;
115 if(a
->pref
== b
->pref
) {
116 if(a
->weight
== b
->weight
)
118 if(a
->weight
< b
->weight
)
122 if(a
->pref
< b
->pref
)
128 * Iterate over a list of PurpleSrvResponseContainer making the sum
129 * the running total of the sums. Select a random integer in the range
130 * (1, sum+1), then find the first element greater than or equal to the
131 * number selected. From RFC 2782.
133 * @param list The list of PurpleSrvResponseContainer. This function
134 * removes a node from this list and returns the new list.
135 * @param container_ptr The PurpleSrvResponseContainer that was chosen
136 * will be returned here.
139 select_random_response(GList
*list
, PurpleSrvResponseContainer
**container_ptr
)
149 PurpleSrvResponseContainer
*container
= cur
->data
;
150 runningtotal
+= container
->response
->weight
;
151 container
->sum
= runningtotal
;
156 * If the running total is greater than 0, pick a number between
157 * 1 and the runningtotal inclusive. (This is not precisely what
158 * the RFC algorithm describes, but we wish to deal with integers
159 * and avoid floats. This is functionally equivalent.)
160 * If running total is 0, then choose r = 0.
162 r
= runningtotal
? g_random_int_range(1, runningtotal
+ 1) : 0;
164 while (r
> ((PurpleSrvResponseContainer
*)cur
->data
)->sum
) {
168 /* Set the return parameter and remove cur from the list */
169 *container_ptr
= cur
->data
;
170 return g_list_delete_link(list
, cur
);
174 * Reorder a GList of PurpleSrvResponses that have the same priority
178 srv_reorder(GList
*list
, int num
)
181 GList
*cur
, *container_list
= NULL
;
182 PurpleSrvResponseContainer
*container
;
185 /* Nothing to sort */
188 /* First build a list of container structs */
189 for (i
= 0, cur
= list
; i
< num
; i
++, cur
= cur
->next
) {
190 container
= g_new(PurpleSrvResponseContainer
, 1);
191 container
->response
= cur
->data
;
192 container_list
= g_list_prepend(container_list
, container
);
194 container_list
= g_list_reverse(container_list
);
197 * Re-order the list that was passed in as a parameter. We leave
198 * the list nodes in place, but replace their data pointers.
201 while (container_list
) {
202 container_list
= select_random_response(container_list
, &container
);
203 cur
->data
= container
->response
;
210 * Sorts a GList of PurpleSrvResponses according to the
211 * algorithm described in RFC 2782.
213 * @param response GList of PurpleSrvResponse's
214 * @param The original list, resorted
217 purple_srv_sort(GList
*list
)
222 if (!list
|| !list
->next
) {
223 /* Nothing to sort */
227 list
= g_list_sort(list
, responsecompare
);
232 PurpleSrvResponse
*next_response
;
233 pref
= ((PurpleSrvResponse
*)cur
->data
)->pref
;
234 next_response
= cur
->next
? cur
->next
->data
: NULL
;
235 if (!next_response
|| next_response
->pref
!= pref
) {
237 * The 'count' records starting at 'start' all have the same
238 * priority. Sort them by weight.
240 srv_reorder(start
, count
);
253 dns_str_is_ascii(const char *name
)
256 for (c
= (guchar
*)name
; c
&& *c
; ++c
) {
267 write_to_parent(int in
, int out
, gconstpointer data
, gsize size
)
269 const guchar
*buf
= data
;
273 w
= write(out
, buf
, size
);
277 } else if (w
< 0 && errno
== EINTR
) {
278 /* Let's try some more; */
281 } while (size
> 0 && w
> 0);
284 /* An error occurred */
291 /* Read size bytes to data. Dies if an error occurs. */
293 read_from_parent(int in
, int out
, gpointer data
, gsize size
)
299 r
= read(in
, data
, size
);
303 } else if (r
< 0 && errno
== EINTR
) {
304 /* Let's try some more; */
307 } while (size
> 0 && r
> 0);
310 /* An error occurred */
318 G_GNUC_NORETURN
static void
319 resolve(int in
, int out
)
322 PurpleSrvResponse
*srvres
;
323 PurpleTxtResponse
*txtres
;
325 int size
, qdcount
, ancount
;
328 guint16 type
, dlen
, pref
, weight
, port
;
329 PurpleSrvInternalQuery query
;
332 purple_restore_default_signal_handlers();
335 read_from_parent(in
, out
, &query
, sizeof(query
));
337 size
= res_query( query
.query
, C_IN
, query
.type
, (u_char
*)&answer
, sizeof( answer
));
339 write_to_parent(in
, out
, &(query
.type
), sizeof(query
.type
));
340 write_to_parent(in
, out
, &size
, sizeof(size
));
346 qdcount
= ntohs(answer
.hdr
.qdcount
);
347 ancount
= ntohs(answer
.hdr
.ancount
);
348 cp
= (guchar
*)&answer
+ sizeof(HEADER
);
349 end
= (guchar
*)&answer
+ size
;
351 /* skip over unwanted stuff */
352 while (qdcount
-- > 0 && cp
< end
) {
353 size
= dn_expand( (unsigned char*)&answer
, end
, cp
, name
, 256);
354 if(size
< 0) goto end
;
355 cp
+= size
+ QFIXEDSZ
;
358 while (ancount
-- > 0 && cp
< end
) {
359 size
= dn_expand((unsigned char*)&answer
, end
, cp
, name
, 256);
365 /* skip ttl and class since we already know it */
376 size
= dn_expand( (unsigned char*)&answer
, end
, cp
, name
, 256);
382 srvres
= g_new0(PurpleSrvResponse
, 1);
383 strcpy(srvres
->hostname
, name
);
386 srvres
->weight
= weight
;
388 ret
= g_list_prepend(ret
, srvres
);
389 } else if (type
== T_TXT
) {
390 txtres
= g_new0(PurpleTxtResponse
, 1);
391 txtres
->content
= g_strndup((gchar
*)(++cp
), dlen
-1);
392 ret
= g_list_append(ret
, txtres
);
400 size
= g_list_length(ret
);
402 if (query
.type
== T_SRV
)
403 ret
= purple_srv_sort(ret
);
405 write_to_parent(in
, out
, &(query
.type
), sizeof(query
.type
));
406 write_to_parent(in
, out
, &size
, sizeof(size
));
409 if (query
.type
== T_SRV
)
410 write_to_parent(in
, out
, ret
->data
, sizeof(PurpleSrvResponse
));
411 if (query
.type
== T_TXT
) {
412 PurpleTxtResponse
*response
= ret
->data
;
413 gsize l
= strlen(response
->content
) + 1 /* null byte */;
414 write_to_parent(in
, out
, &l
, sizeof(l
));
415 write_to_parent(in
, out
, response
->content
, l
);
419 ret
= g_list_remove(ret
, ret
->data
);
429 resolved(gpointer data
, gint source
, PurpleInputCondition cond
)
433 PurpleSrvQueryData
*query_data
= (PurpleSrvQueryData
*)data
;
437 if (read(source
, &type
, sizeof(type
)) == sizeof(type
)) {
438 if (read(source
, &size
, sizeof(size
)) == sizeof(size
)) {
439 if (size
== -1 || size
== 0) {
441 purple_debug_warning("dnssrv", "res_query returned an error\n");
442 /* Re-read resolv.conf and friends in case DNS servers have changed */
445 purple_debug_info("dnssrv", "Found 0 entries, errno is %i\n", errno
);
448 PurpleSrvCallback cb
= query_data
->cb
.srv
;
449 cb(NULL
, 0, query_data
->extradata
);
450 } else if (type
== T_TXT
) {
451 PurpleTxtCallback cb
= query_data
->cb
.txt
;
452 cb(NULL
, query_data
->extradata
);
454 purple_debug_error("dnssrv", "type unknown of DNS result entry; errno is %i\n", errno
);
459 PurpleSrvResponse
*res
;
460 PurpleSrvResponse
*tmp
;
461 PurpleSrvCallback cb
= query_data
->cb
.srv
;
463 purple_debug_info("dnssrv","found %d SRV entries\n", size
);
464 tmp
= res
= g_new0(PurpleSrvResponse
, size
);
465 for (i
= 0; i
< size
; i
++) {
466 red
= read(source
, tmp
++, sizeof(PurpleSrvResponse
));
467 if (red
!= sizeof(PurpleSrvResponse
)) {
468 purple_debug_error("dnssrv","unable to read srv "
469 "response: %s\n", g_strerror(errno
));
476 cb(res
, size
, query_data
->extradata
);
477 } else if (type
== T_TXT
) {
478 GList
*responses
= NULL
;
479 PurpleTxtResponse
*res
;
480 PurpleTxtCallback cb
= query_data
->cb
.txt
;
482 purple_debug_info("dnssrv","found %d TXT entries\n", size
);
483 for (i
= 0; i
< size
; i
++) {
486 red
= read(source
, &len
, sizeof(len
));
487 if (red
!= sizeof(len
)) {
488 purple_debug_error("dnssrv","unable to read txt "
489 "response length: %s\n", g_strerror(errno
));
491 g_list_foreach(responses
, (GFunc
)purple_txt_response_destroy
, NULL
);
492 g_list_free(responses
);
497 res
= g_new0(PurpleTxtResponse
, 1);
498 res
->content
= g_new0(gchar
, len
);
500 red
= read(source
, res
->content
, len
);
502 purple_debug_error("dnssrv","unable to read txt "
503 "response: %s\n", g_strerror(errno
));
505 purple_txt_response_destroy(res
);
506 g_list_foreach(responses
, (GFunc
)purple_txt_response_destroy
, NULL
);
507 g_list_free(responses
);
511 responses
= g_list_prepend(responses
, res
);
514 responses
= g_list_reverse(responses
);
515 cb(responses
, query_data
->extradata
);
517 purple_debug_error("dnssrv", "type unknown of DNS result entry; errno is %i\n", errno
);
523 waitpid(query_data
->pid
, &status
, 0);
524 purple_srv_cancel(query_data
);
529 /** The Jabber Server code was inspiration for parts of this. */
532 res_main_thread_cb(gpointer data
)
534 PurpleSrvResponse
*srvres
= NULL
;
535 PurpleSrvQueryData
*query_data
= data
;
536 if(query_data
->error_message
!= NULL
) {
537 purple_debug_error("dnssrv", query_data
->error_message
);
538 if (query_data
->type
== DNS_TYPE_SRV
) {
539 if (query_data
->cb
.srv
)
540 query_data
->cb
.srv(srvres
, 0, query_data
->extradata
);
541 } else if (query_data
->type
== DNS_TYPE_TXT
) {
542 if (query_data
->cb
.txt
)
543 query_data
->cb
.txt(NULL
, query_data
->extradata
);
546 if (query_data
->type
== DNS_TYPE_SRV
) {
547 PurpleSrvResponse
*srvres_tmp
= NULL
;
548 GList
*lst
= query_data
->results
;
549 int size
= g_list_length(lst
);
551 if(query_data
->cb
.srv
&& size
> 0)
552 srvres_tmp
= srvres
= g_new0(PurpleSrvResponse
, size
);
554 PurpleSrvResponse
*lstdata
= lst
->data
;
555 lst
= g_list_delete_link(lst
, lst
);
557 if(query_data
->cb
.srv
)
558 memcpy(srvres_tmp
++, lstdata
, sizeof(PurpleSrvResponse
));
562 query_data
->results
= NULL
;
564 purple_debug_info("dnssrv", "found %d SRV entries\n", size
);
566 if(query_data
->cb
.srv
) query_data
->cb
.srv(srvres
, size
, query_data
->extradata
);
567 } else if (query_data
->type
== DNS_TYPE_TXT
) {
568 GList
*lst
= query_data
->results
;
570 purple_debug_info("dnssrv", "found %d TXT entries\n", g_list_length(lst
));
572 if (query_data
->cb
.txt
) {
573 query_data
->results
= NULL
;
574 query_data
->cb
.txt(lst
, query_data
->extradata
);
577 purple_debug_error("dnssrv", "unknown query type");
581 query_data
->resolver
= NULL
;
582 query_data
->handle
= 0;
584 purple_srv_cancel(query_data
);
590 res_thread(gpointer data
)
592 PDNS_RECORD dr
= NULL
;
595 PurpleSrvQueryData
*query_data
= data
;
596 type
= query_data
->type
;
597 ds
= MyDnsQuery_UTF8(query_data
->query
, type
, DNS_QUERY_STANDARD
, NULL
, &dr
, NULL
);
598 if (ds
!= ERROR_SUCCESS
) {
599 gchar
*msg
= g_win32_error_message(ds
);
600 if (type
== DNS_TYPE_SRV
) {
601 query_data
->error_message
= g_strdup_printf("Couldn't look up SRV record. %s (%lu).\n", msg
, ds
);
602 } else if (type
== DNS_TYPE_TXT
) {
603 query_data
->error_message
= g_strdup_printf("Couldn't look up TXT record. %s (%lu).\n", msg
, ds
);
607 if (type
== DNS_TYPE_SRV
) {
610 DNS_SRV_DATA
*srv_data
;
611 PurpleSrvResponse
*srvres
;
613 for (dr_tmp
= dr
; dr_tmp
!= NULL
; dr_tmp
= dr_tmp
->pNext
) {
614 /* Discard any incorrect entries. I'm not sure if this is necessary */
615 if (dr_tmp
->wType
!= type
|| strcmp(dr_tmp
->pName
, query_data
->query
) != 0) {
619 srv_data
= &dr_tmp
->Data
.SRV
;
620 srvres
= g_new0(PurpleSrvResponse
, 1);
621 strncpy(srvres
->hostname
, srv_data
->pNameTarget
, 255);
622 srvres
->hostname
[255] = '\0';
623 srvres
->pref
= srv_data
->wPriority
;
624 srvres
->port
= srv_data
->wPort
;
625 srvres
->weight
= srv_data
->wWeight
;
627 lst
= g_list_prepend(lst
, srvres
);
630 MyDnsRecordListFree(dr
, DnsFreeRecordList
);
631 query_data
->results
= purple_srv_sort(lst
);
632 } else if (type
== DNS_TYPE_TXT
) {
635 DNS_TXT_DATA
*txt_data
;
636 PurpleTxtResponse
*txtres
;
638 for (dr_tmp
= dr
; dr_tmp
!= NULL
; dr_tmp
= dr_tmp
->pNext
) {
642 /* Discard any incorrect entries. I'm not sure if this is necessary */
643 if (dr_tmp
->wType
!= type
|| strcmp(dr_tmp
->pName
, query_data
->query
) != 0) {
647 txt_data
= &dr_tmp
->Data
.TXT
;
648 txtres
= g_new0(PurpleTxtResponse
, 1);
650 s
= g_string_new("");
651 for (i
= 0; i
< txt_data
->dwStringCount
; ++i
)
652 s
= g_string_append(s
, txt_data
->pStringArray
[i
]);
653 txtres
->content
= g_string_free(s
, FALSE
);
655 lst
= g_list_append(lst
, txtres
);
658 MyDnsRecordListFree(dr
, DnsFreeRecordList
);
659 query_data
->results
= lst
;
665 /* back to main thread */
666 /* Note: this should *not* be attached to query_data->handle - it will cause leakage */
667 purple_timeout_add(0, res_main_thread_cb
, query_data
);
676 purple_srv_resolve(const char *protocol
, const char *transport
, const char *domain
, PurpleSrvCallback cb
, gpointer extradata
)
680 PurpleSrvQueryData
*query_data
;
682 PurpleSrvInternalQuery internal_query
;
687 static gboolean initialized
= FALSE
;
690 if (!protocol
|| !*protocol
|| !transport
|| !*transport
|| !domain
|| !*domain
) {
691 purple_debug_error("dnssrv", "Wrong arguments\n");
692 cb(NULL
, 0, extradata
);
693 g_return_val_if_reached(NULL
);
697 if (!dns_str_is_ascii(domain
)) {
698 int ret
= purple_network_convert_idn_to_ascii(domain
, &hostname
);
700 purple_debug_error("dnssrv", "IDNA ToASCII failed\n");
701 cb(NULL
, 0, extradata
);
704 } else /* Fallthru is intentional */
706 hostname
= g_strdup(domain
);
708 query
= g_strdup_printf("_%s._%s.%s", protocol
, transport
, hostname
);
709 purple_debug_info("dnssrv","querying SRV record for %s: %s\n", domain
,
714 if(pipe(in
) || pipe(out
)) {
715 purple_debug_error("dnssrv", "Could not create pipe\n");
717 cb(NULL
, 0, extradata
);
723 purple_debug_error("dnssrv", "Could not create process!\n");
724 cb(NULL
, 0, extradata
);
736 resolve(in
[0], out
[1]);
737 /* resolve() does not return */
743 internal_query
.type
= T_SRV
;
744 strncpy(internal_query
.query
, query
, 255);
745 internal_query
.query
[255] = '\0';
747 if (write(in
[1], &internal_query
, sizeof(internal_query
)) < 0)
748 purple_debug_error("dnssrv", "Could not write to SRV resolver\n");
750 query_data
= g_new0(PurpleSrvQueryData
, 1);
751 query_data
->type
= T_SRV
;
752 query_data
->cb
.srv
= cb
;
753 query_data
->extradata
= extradata
;
754 query_data
->pid
= pid
;
755 query_data
->fd_out
= out
[0];
756 query_data
->fd_in
= in
[1];
757 query_data
->handle
= purple_input_add(out
[0], PURPLE_INPUT_READ
, resolved
, query_data
);
764 MyDnsQuery_UTF8
= (void*) wpurple_find_and_loadproc("dnsapi.dll", "DnsQuery_UTF8");
765 MyDnsRecordListFree
= (void*) wpurple_find_and_loadproc(
766 "dnsapi.dll", "DnsRecordListFree");
770 query_data
= g_new0(PurpleSrvQueryData
, 1);
771 query_data
->type
= DNS_TYPE_SRV
;
772 query_data
->cb
.srv
= cb
;
773 query_data
->query
= query
;
774 query_data
->extradata
= extradata
;
776 if (!MyDnsQuery_UTF8
|| !MyDnsRecordListFree
)
777 query_data
->error_message
= g_strdup("System missing DNS API (Requires W2K+)\n");
779 query_data
->resolver
= g_thread_create(res_thread
, query_data
, FALSE
, &err
);
780 if (query_data
->resolver
== NULL
) {
781 query_data
->error_message
= g_strdup_printf("SRV thread create failure: %s\n", (err
&& err
->message
) ? err
->message
: "");
786 /* The query isn't going to happen, so finish the SRV lookup now.
787 * Asynchronously call the callback since stuff may not expect
788 * the callback to be called before this returns */
789 if (query_data
->error_message
!= NULL
)
790 query_data
->handle
= purple_timeout_add(0, res_main_thread_cb
, query_data
);
796 PurpleSrvQueryData
*purple_txt_resolve(const char *owner
, const char *domain
, PurpleTxtCallback cb
, gpointer extradata
)
800 PurpleSrvQueryData
*query_data
;
802 PurpleSrvInternalQuery internal_query
;
807 static gboolean initialized
= FALSE
;
811 if (!dns_str_is_ascii(domain
)) {
812 int ret
= purple_network_convert_idn_to_ascii(domain
, &hostname
);
814 purple_debug_error("dnssrv", "IDNA ToASCII failed\n");
818 } else /* fallthru is intentional */
820 hostname
= g_strdup(domain
);
822 query
= g_strdup_printf("%s.%s", owner
, hostname
);
823 purple_debug_info("dnssrv","querying TXT record for %s: %s\n", domain
,
828 if(pipe(in
) || pipe(out
)) {
829 purple_debug_error("dnssrv", "Could not create pipe\n");
837 purple_debug_error("dnssrv", "Could not create process!\n");
850 resolve(in
[0], out
[1]);
851 /* resolve() does not return */
857 internal_query
.type
= T_TXT
;
858 strncpy(internal_query
.query
, query
, 255);
859 internal_query
.query
[255] = '\0';
861 if (write(in
[1], &internal_query
, sizeof(internal_query
)) < 0)
862 purple_debug_error("dnssrv", "Could not write to TXT resolver\n");
864 query_data
= g_new0(PurpleSrvQueryData
, 1);
865 query_data
->type
= T_TXT
;
866 query_data
->cb
.txt
= cb
;
867 query_data
->extradata
= extradata
;
868 query_data
->pid
= pid
;
869 query_data
->fd_out
= out
[0];
870 query_data
->fd_in
= in
[1];
871 query_data
->handle
= purple_input_add(out
[0], PURPLE_INPUT_READ
, resolved
, query_data
);
878 MyDnsQuery_UTF8
= (void*) wpurple_find_and_loadproc("dnsapi.dll", "DnsQuery_UTF8");
879 MyDnsRecordListFree
= (void*) wpurple_find_and_loadproc(
880 "dnsapi.dll", "DnsRecordListFree");
884 query_data
= g_new0(PurpleSrvQueryData
, 1);
885 query_data
->type
= DNS_TYPE_TXT
;
886 query_data
->cb
.txt
= cb
;
887 query_data
->query
= query
;
888 query_data
->extradata
= extradata
;
890 if (!MyDnsQuery_UTF8
|| !MyDnsRecordListFree
)
891 query_data
->error_message
= g_strdup("System missing DNS API (Requires W2K+)\n");
893 query_data
->resolver
= g_thread_create(res_thread
, query_data
, FALSE
, &err
);
894 if (query_data
->resolver
== NULL
) {
895 query_data
->error_message
= g_strdup_printf("TXT thread create failure: %s\n", (err
&& err
->message
) ? err
->message
: "");
900 /* The query isn't going to happen, so finish the TXT lookup now.
901 * Asynchronously call the callback since stuff may not expect
902 * the callback to be called before this returns */
903 if (query_data
->error_message
!= NULL
)
904 query_data
->handle
= purple_timeout_add(0, res_main_thread_cb
, query_data
);
911 purple_srv_cancel(PurpleSrvQueryData
*query_data
)
913 if (query_data
->handle
> 0)
914 purple_input_remove(query_data
->handle
);
916 if (query_data
->resolver
!= NULL
)
919 * It's not really possible to kill a thread. So instead we
920 * just set the callback to NULL and let the DNS lookup
923 query_data
->cb
.srv
= NULL
;
926 g_free(query_data
->query
);
927 g_free(query_data
->error_message
);
929 close(query_data
->fd_out
);
930 close(query_data
->fd_in
);
936 purple_txt_cancel(PurpleSrvQueryData
*query_data
)
938 purple_srv_cancel(query_data
);
942 purple_txt_response_get_content(PurpleTxtResponse
*resp
)
944 g_return_val_if_fail(resp
!= NULL
, NULL
);
946 return resp
->content
;
949 void purple_txt_response_destroy(PurpleTxtResponse
*resp
)
951 g_return_if_fail(resp
!= NULL
);
953 g_free(resp
->content
);