diff options
author | Luigi Rizzo <rizzo@icir.org> | 2006-10-18 17:45:50 +0000 |
---|---|---|
committer | Luigi Rizzo <rizzo@icir.org> | 2006-10-18 17:45:50 +0000 |
commit | a51816520770e488eaf4594ee851ba55cd192d00 (patch) | |
tree | 1232065bc95add574f8aed0a9e51a4699e0e8958 /main/manager.c | |
parent | a18accc09e883e1dd3b463eedb22dcf6d269bef9 (diff) |
despite the large changes, this commit only moves functions
around so that functions belonging to the same group are
close to each other.
At the beginning of each group i have added a bit of documentation
to explain what the group does and what is the typical flow - basically,
all i have learned by code inspection over the past few days should
be documented for you to read.
I have not put many doxygen annotations just because i am not
sure what are the proper ones. Hopefully some doxygen experts will jump in.
Next on the plate: try to figure out how "struct eventqent"
are supposed to work.
git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@45582 65c4cc65-6c06-0410-ace0-fbb531ad65f3
Diffstat (limited to 'main/manager.c')
-rw-r--r-- | main/manager.c | 632 |
1 files changed, 341 insertions, 291 deletions
diff --git a/main/manager.c b/main/manager.c index 95ec36670..639a5364c 100644 --- a/main/manager.c +++ b/main/manager.c @@ -22,8 +22,15 @@ * * \author Mark Spencer <markster@digium.com> * - * Channel Management and more - * + * At the moment this file contains a number of functions, namely: + * + * - data structures storing AMI state + * - AMI-related API functions, used by internal asterisk components + * - handlers for AMI-related CLI functions + * - handlers for AMI functions (available through the AMI socket) + * - the code for the main AMI listener thread and individual session threads + * - the http handlers invoked for AMI-over-HTTP by the threads in main/http.c + * * \ref amiconf */ @@ -69,22 +76,6 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include "asterisk/threadstorage.h" #include "asterisk/linkedlists.h" -struct fast_originate_helper { - char tech[AST_MAX_MANHEADER_LEN]; - char data[AST_MAX_MANHEADER_LEN]; - int timeout; - char app[AST_MAX_APP]; - char appdata[AST_MAX_MANHEADER_LEN]; - char cid_name[AST_MAX_MANHEADER_LEN]; - char cid_num[AST_MAX_MANHEADER_LEN]; - char context[AST_MAX_CONTEXT]; - char exten[AST_MAX_EXTENSION]; - char idtext[AST_MAX_MANHEADER_LEN]; - char account[AST_MAX_ACCOUNT_CODE]; - int priority; - struct ast_variable *vars; -}; - struct eventqent { int usecount; int category; @@ -128,22 +119,6 @@ AST_THREADSTORAGE(manager_event_buf, manager_event_buf_init); AST_THREADSTORAGE(astman_append_buf, astman_append_buf_init); #define ASTMAN_APPEND_BUF_INITSIZE 256 -static struct permalias { - int num; - char *label; -} perms[] = { - { EVENT_FLAG_SYSTEM, "system" }, - { EVENT_FLAG_CALL, "call" }, - { EVENT_FLAG_LOG, "log" }, - { EVENT_FLAG_VERBOSE, "verbose" }, - { EVENT_FLAG_COMMAND, "command" }, - { EVENT_FLAG_AGENT, "agent" }, - { EVENT_FLAG_USER, "user" }, - { EVENT_FLAG_CONFIG, "config" }, - { -1, "all" }, - { 0, "none" }, -}; - struct mansession { /*! Execution thread */ pthread_t t; @@ -206,6 +181,26 @@ static AST_LIST_HEAD_STATIC(users, ast_manager_user); static struct manager_action *first_action = NULL; AST_MUTEX_DEFINE_STATIC(actionlock); +/* + * helper functions to convert back and forth between + * string and numeric representation of set of flags + */ +static struct permalias { + int num; + char *label; +} perms[] = { + { EVENT_FLAG_SYSTEM, "system" }, + { EVENT_FLAG_CALL, "call" }, + { EVENT_FLAG_LOG, "log" }, + { EVENT_FLAG_VERBOSE, "verbose" }, + { EVENT_FLAG_COMMAND, "command" }, + { EVENT_FLAG_AGENT, "agent" }, + { EVENT_FLAG_USER, "user" }, + { EVENT_FLAG_CONFIG, "config" }, + { -1, "all" }, + { 0, "none" }, +}; + /*! \brief Convert authority code to a list of options */ static char *authority_to_str(int authority, char *res, int reslen) { @@ -227,196 +222,89 @@ static char *authority_to_str(int authority, char *res, int reslen) return res; } -static char *complete_show_mancmd(const char *line, const char *word, int pos, int state) +/*! Tells you if smallstr exists inside bigstr + which is delim by delim and uses no buf or stringsep + ast_instring("this|that|more","this",'|') == 1; + + feel free to move this to app.c -anthm */ +static int ast_instring(const char *bigstr, const char *smallstr, const char delim) { - struct manager_action *cur; - int l = strlen(word), which = 0; - char *ret = NULL; + const char *val = bigstr, *next; - ast_mutex_lock(&actionlock); - for (cur = first_action; cur; cur = cur->next) { /* Walk the list of actions */ - if (!strncasecmp(word, cur->action, l) && ++which > state) { - ret = ast_strdup(cur->action); - break; /* make sure we exit even if ast_strdup() returns NULL */ - } - } - ast_mutex_unlock(&actionlock); + do { + if ((next = strchr(val, delim))) { + if (!strncmp(val, smallstr, (next - val))) + return 1; + else + continue; + } else + return !strcmp(smallstr, val); - return ret; + } while (*(val = (next + 1))); + + return 0; } -/* - * convert to xml with various conversion: - * mode & 1 -> lowercase; - * mode & 2 -> replace non-alphanumeric chars with underscore - */ -static void xml_copy_escape(char **dst, size_t *maxlen, const char *src, int mode) +static int get_perm(const char *instr) { - for ( ; *src && *maxlen > 6; src++) { - if ( (mode & 2) && !isalnum(*src)) { - *(*dst)++ = '_'; - (*maxlen)--; - continue; - } - switch (*src) { - case '<': - strcpy(*dst, "<"); - (*dst) += 4; - *maxlen -= 4; - break; - case '>': - strcpy(*dst, ">"); - (*dst) += 4; - *maxlen -= 4; - break; - case '\"': - strcpy(*dst, """); - (*dst) += 6; - *maxlen -= 6; - break; - case '\'': - strcpy(*dst, "'"); - (*dst) += 6; - *maxlen -= 6; - break; - case '&': - strcpy(*dst, "&"); - (*dst) += 5; - *maxlen -= 5; - break; + int x = 0, ret = 0; - default: - *(*dst)++ = mode ? tolower(*src) : *src; - (*maxlen)--; - } + if (!instr) + return 0; + + for (x = 0; x < (sizeof(perms) / sizeof(perms[0])); x++) { + if (ast_instring(instr, perms[x].label, ',')) + ret |= perms[x].num; } + + return ret; } -/*! \brief Convert the input into XML or HTML. - * The input is supposed to be a sequence of lines of the form - * Name: value - * optionally followed by a blob of unformatted text. - * A blank line is a section separator. Basically, this is a - * mixture of the format of Manager Interface and CLI commands. - * The unformatted text is considered as a single value of a field - * named 'Opaque-data'. - * - * At the moment the output format is the following (but it may - * change depending on future requirements so don't count too - * much on it when writing applications): - * - * General: the unformatted text is used as a value of - * XML output: to be completed - * Each section is within <response type="object" id="xxx"> - * where xxx is taken from ajaxdest variable or defaults to unknown - * Each row is reported as an attribute Name="value" of an XML - * entity named from the variable ajaxobjtype, default to "generic" - * - * HTML output: - * each Name-value pair is output as a single row of a two-column table. - * Sections (blank lines in the input) are separated by a <HR> - * +/* + * A number returns itself, false returns 0, true returns all flags, + * other strings return the flags that are set. */ -static char *xml_translate(char *in, struct ast_variable *vars, enum output_format format) +static int ast_strings_to_mask(const char *string) { - struct ast_variable *v; - char *dest = NULL; - char *out, *tmp, *var, *val; - char *objtype = NULL; - int colons = 0; - int breaks = 0; - size_t len; - int in_data = 0; /* parsing data */ - int escaped = 0; - int inobj = 0; - int x; - int xml = (format == FORMAT_XML); + const char *p; - for (v = vars; v; v = v->next) { - if (!dest && !strcasecmp(v->name, "ajaxdest")) - dest = v->value; - else if (!objtype && !strcasecmp(v->name, "ajaxobjtype")) - objtype = v->value; - } - if (!dest) - dest = "unknown"; - if (!objtype) - objtype = "generic"; + if (ast_strlen_zero(string)) + return -1; - /* determine how large is the response. - * This is a heuristic - counting colons (for headers), - * newlines (for extra arguments), and escaped chars. - * XXX needs to be checked carefully for overflows. - * Even better, use some code that allows extensible strings. - */ - for (x = 0; in[x]; x++) { - if (in[x] == ':') - colons++; - else if (in[x] == '\n') - breaks++; - else if (strchr("&\"<>", in[x])) - escaped++; + for (p = string; *p; p++) + if (*p < '0' || *p > '9') + break; + if (!p) /* all digits */ + return atoi(string); + if (ast_false(string)) + return 0; + if (ast_true(string)) { /* all permissions */ + int x, ret = 0; + for (x=0; x<sizeof(perms) / sizeof(perms[0]); x++) + ret |= perms[x].num; + return ret; } - len = (size_t) (strlen(in) + colons * 5 + breaks * (40 + strlen(dest) + strlen(objtype)) + escaped * 10); /* foo="bar", "<response type=\"object\" id=\"dest\"", "&" */ - out = ast_malloc(len); - if (!out) - return NULL; - tmp = out; - /* we want to stop when we find an empty line */ - while (in && *in) { - in = ast_skip_blanks(in); /* trailing \n from before */ - val = strsep(&in, "\r\n"); /* mark start and end of line */ - ast_trim_blanks(val); - ast_verbose("inobj %d in_data %d line <%s>\n", inobj, in_data, val); - if (ast_strlen_zero(val)) { - if (in_data) { /* close data */ - ast_build_string(&tmp, &len, xml ? "'" : "</td></tr>\n"); - in_data = 0; - } - ast_build_string(&tmp, &len, xml ? " /></response>\n" : - "<tr><td colspan=\"2\"><hr></td></tr>\r\n"); - inobj = 0; - continue; - } - /* we expect Name: value lines */ - if (in_data) { - var = NULL; - } else { - var = strsep(&val, ":"); - if (val) { /* found the field name */ - val = ast_skip_blanks(val); - ast_trim_blanks(var); - } else { /* field name not found, move to opaque mode */ - val = var; - var = "Opaque-data"; - } - } - if (!inobj) { - if (xml) - ast_build_string(&tmp, &len, "<response type='object' id='%s'><%s", dest, objtype); - else - ast_build_string(&tmp, &len, "<body>\n"); - inobj = 1; - } - if (!in_data) { /* build appropriate line start */ - ast_build_string(&tmp, &len, xml ? " " : "<tr><td>"); - xml_copy_escape(&tmp, &len, var, xml ? 1 | 2 : 0); - ast_build_string(&tmp, &len, xml ? "='" : "</td><td>"); - if (!strcmp(var, "Opaque-data")) - in_data = 1; + return get_perm(string); +} +static char *complete_show_mancmd(const char *line, const char *word, int pos, int state) +{ + struct manager_action *cur; + int l = strlen(word), which = 0; + char *ret = NULL; + + ast_mutex_lock(&actionlock); + for (cur = first_action; cur; cur = cur->next) { /* Walk the list of actions */ + if (!strncasecmp(word, cur->action, l) && ++which > state) { + ret = ast_strdup(cur->action); + break; /* make sure we exit even if ast_strdup() returns NULL */ } - xml_copy_escape(&tmp, &len, val, 0); /* data field */ - if (!in_data) - ast_build_string(&tmp, &len, xml ? "'" : "</td></tr>\n"); - else - ast_build_string(&tmp, &len, xml ? "\n" : "<br>\n"); } - if (inobj) - ast_build_string(&tmp, &len, xml ? " /></response>\n" : - "<tr><td colspan=\"2\"><hr></td></tr>\r\n"); - return out; + ast_mutex_unlock(&actionlock); + + return ret; } + static struct ast_manager_user *ast_get_manager_by_name_locked(const char *name) { struct ast_manager_user *user = NULL; @@ -427,27 +315,6 @@ static struct ast_manager_user *ast_get_manager_by_name_locked(const char *name) return user; } -void astman_append(struct mansession *s, const char *fmt, ...) -{ - va_list ap; - struct ast_dynamic_str *buf; - - if (!(buf = ast_dynamic_str_thread_get(&astman_append_buf, ASTMAN_APPEND_BUF_INITSIZE))) - return; - - va_start(ap, fmt); - ast_dynamic_str_thread_set_va(&buf, 0, &astman_append_buf, fmt, ap); - va_end(ap); - - if (s->fd > -1) - ast_carefulwrite(s->fd, buf->str, strlen(buf->str), s->writetimeout); - else { - if (!s->outputstr && !(s->outputstr = ast_calloc(1, sizeof(*s->outputstr)))) - return; - - ast_dynamic_str_append(&s->outputstr, 0, "%s", buf->str); - } -} static int handle_showmancmd(int fd, int argc, char *argv[]) { @@ -730,6 +597,31 @@ struct ast_variable *astman_get_variables(struct message *m) return head; } +/* + * utility functions for creating AMI replies + */ +void astman_append(struct mansession *s, const char *fmt, ...) +{ + va_list ap; + struct ast_dynamic_str *buf; + + if (!(buf = ast_dynamic_str_thread_get(&astman_append_buf, ASTMAN_APPEND_BUF_INITSIZE))) + return; + + va_start(ap, fmt); + ast_dynamic_str_thread_set_va(&buf, 0, &astman_append_buf, fmt, ap); + va_end(ap); + + if (s->fd > -1) + ast_carefulwrite(s->fd, buf->str, strlen(buf->str), s->writetimeout); + else { + if (!s->outputstr && !(s->outputstr = ast_calloc(1, sizeof(*s->outputstr)))) + return; + + ast_dynamic_str_append(&s->outputstr, 0, "%s", buf->str); + } +} + /*! \note NOTE: XXX this comment is unclear and possibly wrong. Callers of astman_send_error(), astman_send_response() or astman_send_ack() must EITHER hold the session lock _or_ be running in an action callback (in which case s->busy will @@ -777,70 +669,7 @@ static void astman_start_ack(struct mansession *s, struct message *m) astman_send_response(s, m, "Success", MSG_MOREDATA); } -/*! Tells you if smallstr exists inside bigstr - which is delim by delim and uses no buf or stringsep - ast_instring("this|that|more","this",'|') == 1; - feel free to move this to app.c -anthm */ -static int ast_instring(const char *bigstr, const char *smallstr, const char delim) -{ - const char *val = bigstr, *next; - - do { - if ((next = strchr(val, delim))) { - if (!strncmp(val, smallstr, (next - val))) - return 1; - else - continue; - } else - return !strcmp(smallstr, val); - - } while (*(val = (next + 1))); - - return 0; -} - -static int get_perm(const char *instr) -{ - int x = 0, ret = 0; - - if (!instr) - return 0; - - for (x = 0; x < (sizeof(perms) / sizeof(perms[0])); x++) { - if (ast_instring(instr, perms[x].label, ',')) - ret |= perms[x].num; - } - - return ret; -} - -/* - * A number returns itself, false returns 0, true returns all flags, - * other strings return the flags that are set. - */ -static int ast_strings_to_mask(const char *string) -{ - const char *p; - - if (ast_strlen_zero(string)) - return -1; - - for (p = string; *p; p++) - if (*p < '0' || *p > '9') - break; - if (!p) /* all digits */ - return atoi(string); - if (ast_false(string)) - return 0; - if (ast_true(string)) { /* all permissions */ - int x, ret = 0; - for (x=0; x<sizeof(perms) / sizeof(perms[0]); x++) - ret |= perms[x].num; - return ret; - } - return get_perm(string); -} /*! \brief Rather than braindead on,off this now can also accept a specific int mask value @@ -858,6 +687,13 @@ static int set_eventmask(struct mansession *s, char *eventmask) return maskint; } +/* + * Here we start with action_ handlers for AMI actions, + * and the internal functions used by them. + * Generally, the handlers are called action_foo() + */ + +/* helper function for action_login() */ static int authenticate(struct mansession *s, struct message *m) { char *user = astman_get_header(m, "Username"); @@ -1006,7 +842,7 @@ static int action_getconfig(struct mansession *s, struct message *m) return 0; } - +/* helper function for action_updateconfig */ static void handle_updates(struct mansession *s, struct message *m, struct ast_config *cfg) { int x; @@ -1570,6 +1406,22 @@ static int action_command(struct mansession *s, struct message *m) } /* helper function for originate */ +struct fast_originate_helper { + char tech[AST_MAX_MANHEADER_LEN]; + char data[AST_MAX_MANHEADER_LEN]; + int timeout; + char app[AST_MAX_APP]; + char appdata[AST_MAX_MANHEADER_LEN]; + char cid_name[AST_MAX_MANHEADER_LEN]; + char cid_num[AST_MAX_MANHEADER_LEN]; + char context[AST_MAX_CONTEXT]; + char exten[AST_MAX_EXTENSION]; + char idtext[AST_MAX_MANHEADER_LEN]; + char account[AST_MAX_ACCOUNT_CODE]; + int priority; + struct ast_variable *vars; +}; + static void *fast_originate(void *data) { struct fast_originate_helper *in = data; @@ -1911,6 +1763,15 @@ static int action_userevent(struct mansession *s, struct message *m) } /* + * Done with the action handlers here, we start with the code in charge + * of accepting connections and serving them. + * accept_thread() forks a new thread for each connection, session_do(), + * which in turn calls get_input() repeatedly until a full message has + * been accumulated, and then invokes process_message() to pass it to + * the appropriate handler. + */ + +/* * Process an AMI message, performing desired action. * Return 0 on success, -1 on error that require the session to be destroyed. */ @@ -2164,6 +2025,10 @@ static void *accept_thread(void *ignore) return NULL; } +/* + * events are appended to a queue from where they + * can be dispatched to clients. + */ static int append_event(const char *str, int category) { struct eventqent *tmp, *prev = NULL; @@ -2237,6 +2102,9 @@ int manager_event(int category, const char *event, const char *fmt, ...) return 0; } +/* + * support functions to register/unregister AMI action handlers, + */ int ast_manager_unregister(char *action) { struct manager_action *cur = first_action, *prev = first_action; @@ -2317,6 +2185,18 @@ int ast_manager_register2(const char *action, int auth, int (*func)(struct manse /*! @} END Doxygen group */ +/* + * The following are support functions for AMI-over-http. + * The common entry point is generic_http_callback(), + * which extracts HTTP header and URI fields and reformats + * them into AMI messages, locates a proper session + * (using the mansession_id Cookie or GET variable), + * and calls process_message() as for regular AMI clients. + * When done, the output (which goes to a temporary file) + * is read back into a buffer and reformatted as desired, + * then fed back to the client over the original socket. + */ + static struct mansession *find_session(unsigned long ident) { struct mansession *s; @@ -2335,7 +2215,6 @@ static struct mansession *find_session(unsigned long ident) return s; } - static void vars2msg(struct message *m, struct ast_variable *vars) { int x; @@ -2347,6 +2226,177 @@ static void vars2msg(struct message *m, struct ast_variable *vars) } } +/* + * convert to xml with various conversion: + * mode & 1 -> lowercase; + * mode & 2 -> replace non-alphanumeric chars with underscore + */ +static void xml_copy_escape(char **dst, size_t *maxlen, const char *src, int mode) +{ + for ( ; *src && *maxlen > 6; src++) { + if ( (mode & 2) && !isalnum(*src)) { + *(*dst)++ = '_'; + (*maxlen)--; + continue; + } + switch (*src) { + case '<': + strcpy(*dst, "<"); + (*dst) += 4; + *maxlen -= 4; + break; + case '>': + strcpy(*dst, ">"); + (*dst) += 4; + *maxlen -= 4; + break; + case '\"': + strcpy(*dst, """); + (*dst) += 6; + *maxlen -= 6; + break; + case '\'': + strcpy(*dst, "'"); + (*dst) += 6; + *maxlen -= 6; + break; + case '&': + strcpy(*dst, "&"); + (*dst) += 5; + *maxlen -= 5; + break; + + default: + *(*dst)++ = mode ? tolower(*src) : *src; + (*maxlen)--; + } + } +} + +/*! \brief Convert the input into XML or HTML. + * The input is supposed to be a sequence of lines of the form + * Name: value + * optionally followed by a blob of unformatted text. + * A blank line is a section separator. Basically, this is a + * mixture of the format of Manager Interface and CLI commands. + * The unformatted text is considered as a single value of a field + * named 'Opaque-data'. + * + * At the moment the output format is the following (but it may + * change depending on future requirements so don't count too + * much on it when writing applications): + * + * General: the unformatted text is used as a value of + * XML output: to be completed + * Each section is within <response type="object" id="xxx"> + * where xxx is taken from ajaxdest variable or defaults to unknown + * Each row is reported as an attribute Name="value" of an XML + * entity named from the variable ajaxobjtype, default to "generic" + * + * HTML output: + * each Name-value pair is output as a single row of a two-column table. + * Sections (blank lines in the input) are separated by a <HR> + * + */ +static char *xml_translate(char *in, struct ast_variable *vars, enum output_format format) +{ + struct ast_variable *v; + char *dest = NULL; + char *out, *tmp, *var, *val; + char *objtype = NULL; + int colons = 0; + int breaks = 0; + size_t len; + int in_data = 0; /* parsing data */ + int escaped = 0; + int inobj = 0; + int x; + int xml = (format == FORMAT_XML); + + for (v = vars; v; v = v->next) { + if (!dest && !strcasecmp(v->name, "ajaxdest")) + dest = v->value; + else if (!objtype && !strcasecmp(v->name, "ajaxobjtype")) + objtype = v->value; + } + if (!dest) + dest = "unknown"; + if (!objtype) + objtype = "generic"; + + /* determine how large is the response. + * This is a heuristic - counting colons (for headers), + * newlines (for extra arguments), and escaped chars. + * XXX needs to be checked carefully for overflows. + * Even better, use some code that allows extensible strings. + */ + for (x = 0; in[x]; x++) { + if (in[x] == ':') + colons++; + else if (in[x] == '\n') + breaks++; + else if (strchr("&\"<>", in[x])) + escaped++; + } + len = (size_t) (strlen(in) + colons * 5 + breaks * (40 + strlen(dest) + strlen(objtype)) + escaped * 10); /* foo="bar", "<response type=\"object\" id=\"dest\"", "&" */ + out = ast_malloc(len); + if (!out) + return NULL; + tmp = out; + /* we want to stop when we find an empty line */ + while (in && *in) { + in = ast_skip_blanks(in); /* trailing \n from before */ + val = strsep(&in, "\r\n"); /* mark start and end of line */ + ast_trim_blanks(val); + ast_verbose("inobj %d in_data %d line <%s>\n", inobj, in_data, val); + if (ast_strlen_zero(val)) { + if (in_data) { /* close data */ + ast_build_string(&tmp, &len, xml ? "'" : "</td></tr>\n"); + in_data = 0; + } + ast_build_string(&tmp, &len, xml ? " /></response>\n" : + "<tr><td colspan=\"2\"><hr></td></tr>\r\n"); + inobj = 0; + continue; + } + /* we expect Name: value lines */ + if (in_data) { + var = NULL; + } else { + var = strsep(&val, ":"); + if (val) { /* found the field name */ + val = ast_skip_blanks(val); + ast_trim_blanks(var); + } else { /* field name not found, move to opaque mode */ + val = var; + var = "Opaque-data"; + } + } + if (!inobj) { + if (xml) + ast_build_string(&tmp, &len, "<response type='object' id='%s'><%s", dest, objtype); + else + ast_build_string(&tmp, &len, "<body>\n"); + inobj = 1; + } + if (!in_data) { /* build appropriate line start */ + ast_build_string(&tmp, &len, xml ? " " : "<tr><td>"); + xml_copy_escape(&tmp, &len, var, xml ? 1 | 2 : 0); + ast_build_string(&tmp, &len, xml ? "='" : "</td><td>"); + if (!strcmp(var, "Opaque-data")) + in_data = 1; + } + xml_copy_escape(&tmp, &len, val, 0); /* data field */ + if (!in_data) + ast_build_string(&tmp, &len, xml ? "'" : "</td></tr>\n"); + else + ast_build_string(&tmp, &len, xml ? "\n" : "<br>\n"); + } + if (inobj) + ast_build_string(&tmp, &len, xml ? " /></response>\n" : + "<tr><td colspan=\"2\"><hr></td></tr>\r\n"); + return out; +} static char *generic_http_callback(enum output_format format, struct sockaddr_in *requestor, const char *uri, |