1/*** 2 This file is part of PulseAudio. 3 4 Copyright 2005-2009 Lennart Poettering 5 6 PulseAudio is free software; you can redistribute it and/or modify 7 it under the terms of the GNU Lesser General Public License as published 8 by the Free Software Foundation; either version 2.1 of the License, 9 or (at your option) any later version. 10 11 PulseAudio is distributed in the hope that it will be useful, but 12 WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 General Public License for more details. 15 16 You should have received a copy of the GNU Lesser General Public License 17 along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. 18***/ 19 20#ifdef HAVE_CONFIG_H 21#include <config.h> 22#endif 23 24#include <stdlib.h> 25#include <stdio.h> 26#include <string.h> 27#include <errno.h> 28 29#include <pulse/util.h> 30#include <pulse/xmalloc.h> 31#include <pulse/timeval.h> 32 33#include <pulsecore/core-util.h> 34#include <pulsecore/ioline.h> 35#include <pulsecore/thread-mq.h> 36#include <pulsecore/macro.h> 37#include <pulsecore/log.h> 38#include <pulsecore/namereg.h> 39#include <pulsecore/cli-text.h> 40#include <pulsecore/shared.h> 41#include <pulsecore/core-error.h> 42#include <pulsecore/mime-type.h> 43 44#include "protocol-http.h" 45 46/* Don't allow more than this many concurrent connections */ 47#define MAX_CONNECTIONS 10 48 49#define URL_ROOT "/" 50#define URL_CSS "/style" 51#define URL_STATUS "/status" 52#define URL_LISTEN "/listen" 53#define URL_LISTEN_SOURCE "/listen/source/" 54 55#define MIME_HTML "text/html; charset=utf-8" 56#define MIME_TEXT "text/plain; charset=utf-8" 57#define MIME_CSS "text/css" 58 59#define HTML_HEADER(t) \ 60 "<?xml version=\"1.0\"?>\n" \ 61 "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n" \ 62 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n" \ 63 " <head>\n" \ 64 " <title>"t"</title>\n" \ 65 " <link rel=\"stylesheet\" type=\"text/css\" href=\"style\"/>\n" \ 66 " </head>\n" \ 67 " <body>\n" 68 69#define HTML_FOOTER \ 70 " </body>\n" \ 71 "</html>\n" 72 73#define RECORD_BUFFER_SECONDS (5) 74#define DEFAULT_SOURCE_LATENCY (300*PA_USEC_PER_MSEC) 75 76enum state { 77 STATE_REQUEST_LINE, 78 STATE_MIME_HEADER, 79 STATE_DATA 80}; 81 82enum method { 83 METHOD_GET, 84 METHOD_HEAD 85}; 86 87struct connection { 88 pa_http_protocol *protocol; 89 pa_iochannel *io; 90 pa_ioline *line; 91 pa_memblockq *output_memblockq; 92 pa_source_output *source_output; 93 pa_client *client; 94 enum state state; 95 char *url; 96 enum method method; 97 pa_module *module; 98}; 99 100struct pa_http_protocol { 101 PA_REFCNT_DECLARE; 102 103 pa_core *core; 104 pa_idxset *connections; 105 106 pa_strlist *servers; 107}; 108 109enum { 110 SOURCE_OUTPUT_MESSAGE_POST_DATA = PA_SOURCE_OUTPUT_MESSAGE_MAX 111}; 112 113/* Called from main context */ 114static void connection_unlink(struct connection *c) { 115 pa_assert(c); 116 117 if (c->source_output) { 118 pa_source_output_unlink(c->source_output); 119 c->source_output->userdata = NULL; 120 pa_source_output_unref(c->source_output); 121 } 122 123 if (c->client) 124 pa_client_free(c->client); 125 126 pa_xfree(c->url); 127 128 if (c->line) 129 pa_ioline_unref(c->line); 130 131 if (c->io) 132 pa_iochannel_free(c->io); 133 134 if (c->output_memblockq) 135 pa_memblockq_free(c->output_memblockq); 136 137 pa_idxset_remove_by_data(c->protocol->connections, c, NULL); 138 139 pa_xfree(c); 140} 141 142/* Called from main context */ 143static int do_write(struct connection *c) { 144 pa_memchunk chunk; 145 ssize_t r; 146 void *p; 147 148 pa_assert(c); 149 150 if (pa_memblockq_peek(c->output_memblockq, &chunk) < 0) 151 return 0; 152 153 pa_assert(chunk.memblock); 154 pa_assert(chunk.length > 0); 155 156 p = pa_memblock_acquire(chunk.memblock); 157 r = pa_iochannel_write(c->io, (uint8_t*) p+chunk.index, chunk.length); 158 pa_memblock_release(chunk.memblock); 159 160 pa_memblock_unref(chunk.memblock); 161 162 if (r < 0) { 163 pa_log("write(): %s", pa_cstrerror(errno)); 164 return -1; 165 } 166 167 pa_memblockq_drop(c->output_memblockq, (size_t) r); 168 169 return 1; 170} 171 172/* Called from main context */ 173static void do_work(struct connection *c) { 174 pa_assert(c); 175 176 if (pa_iochannel_is_hungup(c->io)) 177 goto fail; 178 179 while (pa_iochannel_is_writable(c->io)) { 180 int r = do_write(c); 181 if (r < 0) 182 goto fail; 183 if (r == 0) 184 break; 185 } 186 187 return; 188 189fail: 190 connection_unlink(c); 191} 192 193/* Called from thread context, except when it is not */ 194static int source_output_process_msg(pa_msgobject *m, int code, void *userdata, int64_t offset, pa_memchunk *chunk) { 195 pa_source_output *o = PA_SOURCE_OUTPUT(m); 196 struct connection *c; 197 198 pa_source_output_assert_ref(o); 199 200 if (!(c = o->userdata)) 201 return -1; 202 203 switch (code) { 204 205 case SOURCE_OUTPUT_MESSAGE_POST_DATA: 206 /* While this function is usually called from IO thread 207 * context, this specific command is not! */ 208 pa_memblockq_push_align(c->output_memblockq, chunk); 209 do_work(c); 210 break; 211 212 default: 213 return pa_source_output_process_msg(m, code, userdata, offset, chunk); 214 } 215 216 return 0; 217} 218 219/* Called from thread context */ 220static void source_output_push_cb(pa_source_output *o, const pa_memchunk *chunk) { 221 struct connection *c; 222 223 pa_source_output_assert_ref(o); 224 pa_assert_se(c = o->userdata); 225 pa_assert(chunk); 226 227 pa_asyncmsgq_post(pa_thread_mq_get()->outq, PA_MSGOBJECT(o), SOURCE_OUTPUT_MESSAGE_POST_DATA, NULL, 0, chunk, NULL); 228} 229 230/* Called from main context */ 231static void source_output_kill_cb(pa_source_output *o) { 232 struct connection*c; 233 234 pa_source_output_assert_ref(o); 235 pa_assert_se(c = o->userdata); 236 237 connection_unlink(c); 238} 239 240/* Called from main context */ 241static pa_usec_t source_output_get_latency_cb(pa_source_output *o) { 242 struct connection*c; 243 244 pa_source_output_assert_ref(o); 245 pa_assert_se(c = o->userdata); 246 247 return pa_bytes_to_usec(pa_memblockq_get_length(c->output_memblockq), &c->source_output->sample_spec); 248} 249 250/*** client callbacks ***/ 251static void client_kill_cb(pa_client *client) { 252 struct connection*c; 253 254 pa_assert(client); 255 pa_assert_se(c = client->userdata); 256 257 connection_unlink(c); 258} 259 260/*** pa_iochannel callbacks ***/ 261static void io_callback(pa_iochannel*io, void *userdata) { 262 struct connection *c = userdata; 263 264 pa_assert(c); 265 pa_assert(io); 266 267 do_work(c); 268} 269 270static char *escape_html(const char *t) { 271 pa_strbuf *sb; 272 const char *p, *e; 273 274 sb = pa_strbuf_new(); 275 276 for (e = p = t; *p; p++) { 277 278 if (*p == '>' || *p == '<' || *p == '&') { 279 280 if (p > e) { 281 pa_strbuf_putsn(sb, e, p-e); 282 e = p + 1; 283 } 284 285 if (*p == '>') 286 pa_strbuf_puts(sb, ">"); 287 else if (*p == '<') 288 pa_strbuf_puts(sb, "<"); 289 else 290 pa_strbuf_puts(sb, "&"); 291 } 292 } 293 294 if (p > e) 295 pa_strbuf_putsn(sb, e, p-e); 296 297 return pa_strbuf_to_string_free(sb); 298} 299 300static void http_response( 301 struct connection *c, 302 int code, 303 const char *msg, 304 const char *mime) { 305 306 char *s; 307 308 pa_assert(c); 309 pa_assert(msg); 310 pa_assert(mime); 311 312 s = pa_sprintf_malloc( 313 "HTTP/1.0 %i %s\n" 314 "Connection: close\n" 315 "Content-Type: %s\n" 316 "Cache-Control: no-cache\n" 317 "Expires: 0\n" 318 "Server: "PACKAGE_NAME"/"PACKAGE_VERSION"\n" 319 "\n", code, msg, mime); 320 pa_ioline_puts(c->line, s); 321 pa_xfree(s); 322} 323 324static void html_response( 325 struct connection *c, 326 int code, 327 const char *msg, 328 const char *text) { 329 330 char *s; 331 pa_assert(c); 332 333 http_response(c, code, msg, MIME_HTML); 334 335 if (c->method == METHOD_HEAD) { 336 pa_ioline_defer_close(c->line); 337 return; 338 } 339 340 if (!text) 341 text = msg; 342 343 s = pa_sprintf_malloc( 344 HTML_HEADER("%s") 345 "%s" 346 HTML_FOOTER, 347 text, text); 348 349 pa_ioline_puts(c->line, s); 350 pa_xfree(s); 351 352 pa_ioline_defer_close(c->line); 353} 354 355static void html_print_field(pa_ioline *line, const char *left, const char *right) { 356 char *eleft, *eright; 357 358 eleft = escape_html(left); 359 eright = escape_html(right); 360 361 pa_ioline_printf(line, 362 "<tr><td><b>%s</b></td>" 363 "<td>%s</td></tr>\n", eleft, eright); 364 365 pa_xfree(eleft); 366 pa_xfree(eright); 367} 368 369static void handle_root(struct connection *c) { 370 char *t; 371 372 pa_assert(c); 373 374 http_response(c, 200, "OK", MIME_HTML); 375 376 if (c->method == METHOD_HEAD) { 377 pa_ioline_defer_close(c->line); 378 return; 379 } 380 381 pa_ioline_puts(c->line, 382 HTML_HEADER(PACKAGE_NAME" "PACKAGE_VERSION) 383 "<h1>"PACKAGE_NAME" "PACKAGE_VERSION"</h1>\n" 384 "<table>\n"); 385 386 t = pa_get_user_name_malloc(); 387 html_print_field(c->line, "User Name:", t); 388 pa_xfree(t); 389 390 t = pa_get_host_name_malloc(); 391 html_print_field(c->line, "Host name:", t); 392 pa_xfree(t); 393 394 t = pa_machine_id(); 395 html_print_field(c->line, "Machine ID:", t); 396 pa_xfree(t); 397 398 t = pa_uname_string(); 399 html_print_field(c->line, "System:", t); 400 pa_xfree(t); 401 402 t = pa_sprintf_malloc("%lu", (unsigned long) getpid()); 403 html_print_field(c->line, "Process ID:", t); 404 pa_xfree(t); 405 406 pa_ioline_puts(c->line, 407 "</table>\n" 408 "<p><a href=\"" URL_STATUS "\">Show an extensive server status report</a></p>\n" 409 "<p><a href=\"" URL_LISTEN "\">Monitor sinks and sources</a></p>\n" 410 HTML_FOOTER); 411 412 pa_ioline_defer_close(c->line); 413} 414 415static void handle_css(struct connection *c) { 416 pa_assert(c); 417 418 http_response(c, 200, "OK", MIME_CSS); 419 420 if (c->method == METHOD_HEAD) { 421 pa_ioline_defer_close(c->line); 422 return; 423 } 424 425 pa_ioline_puts(c->line, 426 "body { color: black; background-color: white; }\n" 427 "a:link, a:visited { color: #900000; }\n" 428 "div.news-date { font-size: 80%; font-style: italic; }\n" 429 "pre { background-color: #f0f0f0; padding: 0.4cm; }\n" 430 ".grey { color: #8f8f8f; font-size: 80%; }" 431 "table { margin-left: 1cm; border:1px solid lightgrey; padding: 0.2cm; }\n" 432 "td { padding-left:10px; padding-right:10px; }\n"); 433 434 pa_ioline_defer_close(c->line); 435} 436 437static void handle_status(struct connection *c) { 438 char *r; 439 440 pa_assert(c); 441 442 http_response(c, 200, "OK", MIME_TEXT); 443 444 if (c->method == METHOD_HEAD) { 445 pa_ioline_defer_close(c->line); 446 return; 447 } 448 449 r = pa_full_status_string(c->protocol->core); 450 pa_ioline_puts(c->line, r); 451 pa_xfree(r); 452 453 pa_ioline_defer_close(c->line); 454} 455 456static void handle_listen(struct connection *c) { 457 pa_source *source; 458 pa_sink *sink; 459 uint32_t idx; 460 461 http_response(c, 200, "OK", MIME_HTML); 462 463 pa_ioline_puts(c->line, 464 HTML_HEADER("Listen") 465 "<h2>Sinks</h2>\n" 466 "<p>\n"); 467 468 if (c->method == METHOD_HEAD) { 469 pa_ioline_defer_close(c->line); 470 return; 471 } 472 473 PA_IDXSET_FOREACH(sink, c->protocol->core->sinks, idx) { 474 char *t, *m; 475 476 t = escape_html(pa_strna(pa_proplist_gets(sink->proplist, PA_PROP_DEVICE_DESCRIPTION))); 477 m = pa_sample_spec_to_mime_type_mimefy(&sink->sample_spec, &sink->channel_map); 478 479 pa_ioline_printf(c->line, 480 "<a href=\"" URL_LISTEN_SOURCE "%s\" title=\"%s\">%s</a><br/>\n", 481 sink->monitor_source->name, m, t); 482 483 pa_xfree(t); 484 pa_xfree(m); 485 } 486 487 pa_ioline_puts(c->line, 488 "</p>\n" 489 "<h2>Sources</h2>\n" 490 "<p>\n"); 491 492 PA_IDXSET_FOREACH(source, c->protocol->core->sources, idx) { 493 char *t, *m; 494 495 if (source->monitor_of) 496 continue; 497 498 t = escape_html(pa_strna(pa_proplist_gets(source->proplist, PA_PROP_DEVICE_DESCRIPTION))); 499 m = pa_sample_spec_to_mime_type_mimefy(&source->sample_spec, &source->channel_map); 500 501 pa_ioline_printf(c->line, 502 "<a href=\"" URL_LISTEN_SOURCE "%s\" title=\"%s\">%s</a><br/>\n", 503 source->name, m, t); 504 505 pa_xfree(m); 506 pa_xfree(t); 507 508 } 509 510 pa_ioline_puts(c->line, 511 "</p>\n" 512 HTML_FOOTER); 513 514 pa_ioline_defer_close(c->line); 515} 516 517static void line_drain_callback(pa_ioline *l, void *userdata) { 518 struct connection *c; 519 520 pa_assert(l); 521 pa_assert_se(c = userdata); 522 523 /* We don't need the line reader anymore, instead we need a real 524 * binary io channel */ 525 pa_assert_se(c->io = pa_ioline_detach_iochannel(c->line)); 526 pa_iochannel_set_callback(c->io, io_callback, c); 527 528 pa_iochannel_socket_set_sndbuf(c->io, pa_memblockq_get_length(c->output_memblockq)); 529 530 pa_ioline_unref(c->line); 531 c->line = NULL; 532} 533 534static void handle_listen_prefix(struct connection *c, const char *source_name) { 535 pa_source *source; 536 pa_source_output_new_data data; 537 pa_sample_spec ss; 538 pa_channel_map cm; 539 char *t; 540 size_t l; 541 542 pa_assert(c); 543 pa_assert(source_name); 544 545 pa_assert(c->line); 546 pa_assert(!c->io); 547 548 if (!(source = pa_namereg_get(c->protocol->core, source_name, PA_NAMEREG_SOURCE))) { 549 html_response(c, 404, "Source not found", NULL); 550 return; 551 } 552 553 ss = source->sample_spec; 554 cm = source->channel_map; 555 556 pa_sample_spec_mimefy(&ss, &cm); 557 558 pa_source_output_new_data_init(&data); 559 data.driver = __FILE__; 560 data.module = c->module; 561 data.client = c->client; 562 pa_source_output_new_data_set_source(&data, source, false, true); 563 pa_proplist_update(data.proplist, PA_UPDATE_MERGE, c->client->proplist); 564 pa_source_output_new_data_set_sample_spec(&data, &ss); 565 pa_source_output_new_data_set_channel_map(&data, &cm); 566 567 pa_source_output_new(&c->source_output, c->protocol->core, &data); 568 pa_source_output_new_data_done(&data); 569 570 if (!c->source_output) { 571 html_response(c, 403, "Cannot create source output", NULL); 572 return; 573 } 574 575 c->source_output->parent.process_msg = source_output_process_msg; 576 c->source_output->push = source_output_push_cb; 577 c->source_output->kill = source_output_kill_cb; 578 c->source_output->get_latency = source_output_get_latency_cb; 579 c->source_output->userdata = c; 580 581 pa_source_output_set_requested_latency(c->source_output, DEFAULT_SOURCE_LATENCY); 582 583 l = (size_t) (pa_bytes_per_second(&ss)*RECORD_BUFFER_SECONDS); 584 c->output_memblockq = pa_memblockq_new( 585 "http protocol connection output_memblockq", 586 0, 587 l, 588 0, 589 &ss, 590 1, 591 0, 592 0, 593 NULL); 594 595 pa_source_output_put(c->source_output); 596 597 t = pa_sample_spec_to_mime_type(&ss, &cm); 598 http_response(c, 200, "OK", t); 599 pa_xfree(t); 600 601 if (c->method == METHOD_HEAD) { 602 connection_unlink(c); 603 return; 604 } 605 pa_ioline_set_callback(c->line, NULL, NULL); 606 607 if (pa_ioline_is_drained(c->line)) 608 line_drain_callback(c->line, c); 609 else 610 pa_ioline_set_drain_callback(c->line, line_drain_callback, c); 611} 612 613static void handle_url(struct connection *c) { 614 pa_assert(c); 615 616 pa_log_debug("Request for %s", c->url); 617 618 if (pa_streq(c->url, URL_ROOT)) 619 handle_root(c); 620 else if (pa_streq(c->url, URL_CSS)) 621 handle_css(c); 622 else if (pa_streq(c->url, URL_STATUS)) 623 handle_status(c); 624 else if (pa_streq(c->url, URL_LISTEN)) 625 handle_listen(c); 626 else if (pa_startswith(c->url, URL_LISTEN_SOURCE)) 627 handle_listen_prefix(c, c->url + sizeof(URL_LISTEN_SOURCE)-1); 628 else 629 html_response(c, 404, "Not Found", NULL); 630} 631 632static void line_callback(pa_ioline *line, const char *s, void *userdata) { 633 struct connection *c = userdata; 634 pa_assert(line); 635 pa_assert(c); 636 637 if (!s) { 638 /* EOF */ 639 connection_unlink(c); 640 return; 641 } 642 643 switch (c->state) { 644 case STATE_REQUEST_LINE: { 645 if (pa_startswith(s, "GET ")) { 646 c->method = METHOD_GET; 647 s +=4; 648 } else if (pa_startswith(s, "HEAD ")) { 649 c->method = METHOD_HEAD; 650 s +=5; 651 } else { 652 goto fail; 653 } 654 655 c->url = pa_xstrndup(s, strcspn(s, " \r\n\t?")); 656 c->state = STATE_MIME_HEADER; 657 break; 658 } 659 660 case STATE_MIME_HEADER: { 661 662 /* Ignore MIME headers */ 663 if (strcspn(s, " \r\n") != 0) 664 break; 665 666 /* We're done */ 667 c->state = STATE_DATA; 668 669 handle_url(c); 670 break; 671 } 672 673 default: 674 ; 675 } 676 677 return; 678 679fail: 680 html_response(c, 500, "Internal Server Error", NULL); 681} 682 683void pa_http_protocol_connect(pa_http_protocol *p, pa_iochannel *io, pa_module *m) { 684 struct connection *c; 685 pa_client_new_data client_data; 686 char pname[128]; 687 688 pa_assert(p); 689 pa_assert(io); 690 pa_assert(m); 691 692 if (pa_idxset_size(p->connections)+1 > MAX_CONNECTIONS) { 693 pa_log("Warning! Too many connections (%u), dropping incoming connection.", MAX_CONNECTIONS); 694 pa_iochannel_free(io); 695 return; 696 } 697 698 c = pa_xnew0(struct connection, 1); 699 c->protocol = p; 700 c->state = STATE_REQUEST_LINE; 701 c->module = m; 702 703 c->line = pa_ioline_new(io); 704 pa_ioline_set_callback(c->line, line_callback, c); 705 706 pa_client_new_data_init(&client_data); 707 client_data.module = c->module; 708 client_data.driver = __FILE__; 709 pa_iochannel_socket_peer_to_string(io, pname, sizeof(pname)); 710 pa_proplist_setf(client_data.proplist, PA_PROP_APPLICATION_NAME, "HTTP client (%s)", pname); 711 pa_proplist_sets(client_data.proplist, "http-protocol.peer", pname); 712 c->client = pa_client_new(p->core, &client_data); 713 pa_client_new_data_done(&client_data); 714 715 if (!c->client) 716 goto fail; 717 718 c->client->kill = client_kill_cb; 719 c->client->userdata = c; 720 721 pa_idxset_put(p->connections, c, NULL); 722 723 return; 724 725fail: 726 if (c) 727 connection_unlink(c); 728} 729 730void pa_http_protocol_disconnect(pa_http_protocol *p, pa_module *m) { 731 struct connection *c; 732 uint32_t idx; 733 734 pa_assert(p); 735 pa_assert(m); 736 737 PA_IDXSET_FOREACH(c, p->connections, idx) 738 if (c->module == m) 739 connection_unlink(c); 740} 741 742static pa_http_protocol* http_protocol_new(pa_core *c) { 743 pa_http_protocol *p; 744 745 pa_assert(c); 746 747 p = pa_xnew0(pa_http_protocol, 1); 748 PA_REFCNT_INIT(p); 749 p->core = c; 750 p->connections = pa_idxset_new(NULL, NULL); 751 752 pa_assert_se(pa_shared_set(c, "http-protocol", p) >= 0); 753 754 return p; 755} 756 757pa_http_protocol* pa_http_protocol_get(pa_core *c) { 758 pa_http_protocol *p; 759 760 if ((p = pa_shared_get(c, "http-protocol"))) 761 return pa_http_protocol_ref(p); 762 763 return http_protocol_new(c); 764} 765 766pa_http_protocol* pa_http_protocol_ref(pa_http_protocol *p) { 767 pa_assert(p); 768 pa_assert(PA_REFCNT_VALUE(p) >= 1); 769 770 PA_REFCNT_INC(p); 771 772 return p; 773} 774 775void pa_http_protocol_unref(pa_http_protocol *p) { 776 struct connection *c; 777 778 pa_assert(p); 779 pa_assert(PA_REFCNT_VALUE(p) >= 1); 780 781 if (PA_REFCNT_DEC(p) > 0) 782 return; 783 784 while ((c = pa_idxset_first(p->connections, NULL))) 785 connection_unlink(c); 786 787 pa_idxset_free(p->connections, NULL); 788 789 pa_strlist_free(p->servers); 790 791 pa_assert_se(pa_shared_remove(p->core, "http-protocol") >= 0); 792 793 pa_xfree(p); 794} 795 796void pa_http_protocol_add_server_string(pa_http_protocol *p, const char *name) { 797 pa_assert(p); 798 pa_assert(PA_REFCNT_VALUE(p) >= 1); 799 pa_assert(name); 800 801 p->servers = pa_strlist_prepend(p->servers, name); 802} 803 804void pa_http_protocol_remove_server_string(pa_http_protocol *p, const char *name) { 805 pa_assert(p); 806 pa_assert(PA_REFCNT_VALUE(p) >= 1); 807 pa_assert(name); 808 809 p->servers = pa_strlist_remove(p->servers, name); 810} 811 812pa_strlist *pa_http_protocol_servers(pa_http_protocol *p) { 813 pa_assert(p); 814 pa_assert(PA_REFCNT_VALUE(p) >= 1); 815 816 return p->servers; 817} 818