Browse Source

[core] handle Connection: Upgrade

personal/stbuehler/wip
Stefan Bühler 9 years ago
parent
commit
5e4a94b0c6
  1. 5
      include/lighttpd/connection.h
  2. 11
      include/lighttpd/http_headers.h
  3. 2
      include/lighttpd/response.h
  4. 5
      include/lighttpd/virtualrequest.h
  5. 48
      src/main/connection.c
  6. 118
      src/main/http_headers.c
  7. 14
      src/main/response.c
  8. 53
      src/main/stream_http_response.c
  9. 8
      src/main/subrequest_lua.c
  10. 16
      src/main/virtualrequest.c
  11. 4
      src/modules/fastcgi_stream.c
  12. 10
      src/modules/mod_proxy.c
  13. 24
      src/modules/mod_status.c

5
include/lighttpd/connection.h

@ -26,8 +26,11 @@ typedef enum {
/** write remaining bytes from raw_out, mainvr finished (or not started) */
LI_CON_STATE_WRITE,
/** connection was upgraded */
LI_CON_STATE_UPGRADED
} liConnectionState;
#define LI_CON_STATE_LAST LI_CON_STATE_WRITE
#define LI_CON_STATE_LAST LI_CON_STATE_UPGRADED
/* update mod_status too */
typedef struct liConnectionSocketCallbacks liConnectionSocketCallbacks;

11
include/lighttpd/http_headers.h

@ -23,6 +23,12 @@ struct liHttpHeaders {
GQueue entries;
};
typedef struct liHttpHeaderTokenizer liHttpHeaderTokenizer;
struct liHttpHeaderTokenizer {
GList *cur;
guint pos;
};
/* strings always get copied, so you should free key and value yourself */
LI_API liHttpHeaders* li_http_headers_new(void);
@ -59,4 +65,9 @@ INLINE gboolean li_http_header_key_is(liHttpHeader *h, const gchar *key, size_t
return (h->keylen == keylen && 0 == g_ascii_strncasecmp(key, h->data->str, keylen));
}
/* very simple tokenizer. splits at ' ' and ',', unquotes \\ escapes and "..." tokens */
LI_API void li_http_header_tokenizer_start(liHttpHeaderTokenizer *tokenizer, liHttpHeaders *headers, const gchar *key, size_t keylen);
LI_API gboolean li_http_header_tokenizer_next(liHttpHeaderTokenizer *tokenizer, GString *token);
#endif

2
include/lighttpd/response.h

@ -15,6 +15,6 @@ LI_API void li_response_init(liResponse *resp);
LI_API void li_response_reset(liResponse *resp);
LI_API void li_response_clear(liResponse *resp);
LI_API void li_response_send_headers(liVRequest *vr, liChunkQueue *raw_out, liChunkQueue *response_body);
LI_API void li_response_send_headers(liVRequest *vr, liChunkQueue *raw_out, liChunkQueue *response_body, gboolean upgraded);
#endif

5
include/lighttpd/virtualrequest.h

@ -36,10 +36,12 @@ typedef enum {
typedef void (*liVRequestHandlerCB)(liVRequest *vr);
typedef liThrottleState* (*liVRequestThrottleCB)(liVRequest *vr);
typedef void (*liVRequestConnectionUpgradeCB)(liVRequest *vr, liStream *backend_drain, liStream *backend_source);
struct liConCallbacks {
liVRequestHandlerCB handle_response_error; /* this is _not_ for 500 - internal error */
liVRequestThrottleCB throttle_out, throttle_in;
liVRequestConnectionUpgradeCB connection_upgrade;
};
/* this data "belongs" to a vrequest, but is updated by the connection code */
@ -167,6 +169,9 @@ LI_API void li_vrequest_indirect_connect(liVRequest *vr, liStream *backend_drain
/* received all response headers/status code - call once from your indirect handler */
LI_API void li_vrequest_indirect_headers_ready(liVRequest *vr);
/* call instead of headers_ready */
LI_API void li_vrequest_connection_upgrade(liVRequest *vr, liStream *backend_drain, liStream *backend_source);
/* Signals an internal error; handles the error in the _next_ loop */
LI_API void li_vrequest_error(liVRequest *vr);

48
src/main/connection.c

@ -129,7 +129,6 @@ static liThrottleState* simple_tcp_throttle_in(liConnection *con) {
return data->sock_stream->throttle_in;
}
static const liConnectionSocketCallbacks simple_tcp_cbs = {
simple_tcp_finished,
simple_tcp_throttle_out,
@ -220,6 +219,12 @@ static void _connection_http_in_cb(liStream *stream, liStreamEvent event) {
if (0 == raw_in->length) return; /* no (new) data */
if (LI_CON_STATE_UPGRADED == con->state) {
li_chunkqueue_steal_all(in, raw_in);
li_stream_notify(stream);
return;
}
if (con->state == LI_CON_STATE_KEEP_ALIVE) {
/* stop keep alive timeout watchers */
if (con->keep_alive_data.link) {
@ -407,7 +412,7 @@ static void _connection_http_out_cb(liStream *stream, liStreamEvent event) {
VR_DEBUG(vr, "%s", "write response headers");
}
con->response_headers_sent = TRUE;
li_response_send_headers(vr, raw_out, out);
li_response_send_headers(vr, raw_out, out, FALSE);
}
if (!con->out_has_all_data && !raw_out->is_closed && NULL != out) {
@ -573,10 +578,45 @@ static liThrottleState* mainvr_throttle_in(liVRequest *vr) {
return con->con_sock.callbacks->throttle_in(con);
}
static void mainvr_connection_upgrade(liVRequest *vr, liStream *backend_drain, liStream *backend_source) {
liConnection* con = li_connection_from_vrequest(vr);
assert(NULL != con);
if (con->response_headers_sent || NULL != con->out.source) {
li_connection_error(con);
return;
}
if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
VR_DEBUG(vr, "%s", "connection upgrade: write response headers");
}
con->response_headers_sent = TRUE;
con->info.keep_alive = FALSE;
li_response_send_headers(vr, con->out.out, NULL, TRUE);
con->state = LI_CON_STATE_UPGRADED;
vr->response.transfer_encoding = 0;
li_stream_disconnect_dest(&con->in);
con->in.out->is_closed = FALSE;
li_stream_connect(&con->in, backend_drain);
li_stream_connect(backend_source, &con->out);
li_vrequest_reset(con->mainvr, TRUE);
if (NULL != con->in.source) {
li_chunkqueue_steal_all(con->out.out, backend_drain->out);
}
con->info.out_queue_length = con->out.out->length;
li_stream_notify(&con->out);
li_stream_notify(&con->in);
}
static const liConCallbacks con_callbacks = {
mainvr_handle_response_error,
mainvr_throttle_out,
mainvr_throttle_in
mainvr_throttle_in,
mainvr_connection_upgrade
};
liConnection* li_connection_new(liWorker *wrk) {
@ -815,6 +855,8 @@ gchar *li_connection_state_str(liConnectionState state) {
return "handle main vrequest";
case LI_CON_STATE_WRITE:
return "write";
case LI_CON_STATE_UPGRADED:
return "upgraded";
}
return "undefined";

118
src/main/http_headers.c

@ -7,6 +7,45 @@ static void _http_header_free(gpointer p) {
g_slice_free(liHttpHeader, h);
}
/* remove folding */
static void _http_header_sanitize(liHttpHeader *h) {
guint i, j, len = h->data->len;
gboolean folding = FALSE;
char *str = h->data->str;
for (i = h->keylen + 2; i < len; ++i) {
switch (str[i]) {
case '\r':
case '\n':
goto cleanup;
default:
break;
}
}
return;
cleanup:
for (j = i; i < len; ++i) {
switch (str[i]) {
case '\r':
case '\n':
folding = TRUE;
break;
case ' ':
case '\t':
if (!folding) str[j++] = str[i];
break;
default:
if (folding) {
str[j++] = ' ';
folding = FALSE;
}
str[j++] = str[i];
break;
}
}
g_string_truncate(h->data, j);
}
static liHttpHeader* _http_header_new(const gchar *key, size_t keylen, const gchar *val, size_t valuelen) {
liHttpHeader *h = g_slice_new0(liHttpHeader);
gchar *s;
@ -19,6 +58,7 @@ static liHttpHeader* _http_header_new(const gchar *key, size_t keylen, const gch
memcpy(s, ": ", 2);
s += 2;
memcpy(s, val, valuelen);
_http_header_sanitize(h);
return h;
}
@ -173,3 +213,81 @@ void li_http_header_get_all(GString *dest, liHttpHeaders *headers, const gchar *
g_string_append_len(dest, &h->data->str[h->keylen+2], h->data->len - (h->keylen + 2));
}
}
void li_http_header_tokenizer_start(liHttpHeaderTokenizer *tokenizer, liHttpHeaders *headers, const gchar *key, size_t keylen) {
if (NULL != (tokenizer->cur = li_http_header_find_first(headers, key, keylen))) {
liHttpHeader *h = (liHttpHeader*) tokenizer->cur->data;
tokenizer->pos = h->keylen + 2;
} else {
tokenizer->pos = 0;
}
}
gboolean li_http_header_tokenizer_next(liHttpHeaderTokenizer *tokenizer, GString *token) {
liHttpHeader *h;
guint len;
guint pos = tokenizer->pos;
gchar *str;
g_string_truncate(token, 0);
if (NULL == tokenizer->cur) return FALSE;
h = (liHttpHeader*) tokenizer->cur->data;
len = h->data->len;
str = h->data->str;
for (;;++pos) {
while (pos >= len) {
if (token->len > 0) {
tokenizer->pos = pos;
return TRUE;
}
if (NULL != (tokenizer->cur = li_http_header_find_next(tokenizer->cur, LI_HEADER_KEY_LEN(h)))) {
h = (liHttpHeader*) tokenizer->cur->data;
pos = tokenizer->pos = h->keylen + 2;
len = h->data->len;
str = h->data->str;
} else {
tokenizer->pos = 0;
return FALSE;
}
}
switch (str[pos]) {
case '"':
++pos;
if (token->len > 0) return FALSE; /* either the complete token is quoted or nothing */
goto quoted;
case ' ':
case ',':
if (token->len == 0) continue;
tokenizer->pos = pos+1;
return TRUE;
case '\\':
++pos;
if (pos >= len) return FALSE; /* no character after backslash */
/* fall through, append whatever comes */
default:
g_string_append_c(token, str[pos]);
break;
}
}
quoted:
for (; pos < len; ++pos) {
switch (str[pos]) {
case '"':
++pos;
tokenizer->pos = pos;
return TRUE;
case '\\':
++pos;
if (pos >= len) return FALSE; /* no character after backslash */
/* fall through, append whatever comes */
default:
g_string_append_c(token, str[pos]);
break;
}
}
return FALSE; /* no terminating quote found */
}

14
src/main/response.c

@ -24,7 +24,7 @@ void li_response_clear(liResponse *resp) {
static void li_response_send_error_page(liVRequest *vr, liChunkQueue *response_body);
void li_response_send_headers(liVRequest *vr, liChunkQueue *raw_out, liChunkQueue *response_body) {
void li_response_send_headers(liVRequest *vr, liChunkQueue *raw_out, liChunkQueue *response_body, gboolean upgraded) {
GString *head;
gboolean have_real_body, response_complete;
liChunkQueue *tmp_cq = NULL;
@ -53,8 +53,8 @@ void li_response_send_headers(liVRequest *vr, liChunkQueue *raw_out, liChunkQueu
vr->response.http_status == 205 ||
vr->response.http_status == 304) {
/* They never have a content-body/length */
li_chunkqueue_skip_all(response_body);
raw_out->is_closed = TRUE;
if (NULL != response_body) li_chunkqueue_skip_all(response_body);
if (!upgraded) raw_out->is_closed = TRUE;
} else if (response_complete) {
if (vr->request.http_method != LI_HTTP_METHOD_HEAD || response_body->length > 0) {
/* do not send content-length: 0 if backend already skipped content generation for HEAD */
@ -74,8 +74,8 @@ void li_response_send_headers(liVRequest *vr, liChunkQueue *raw_out, liChunkQueu
if (vr->request.http_method == LI_HTTP_METHOD_HEAD) {
/* content-length is set, but no body */
li_chunkqueue_skip_all(response_body);
raw_out->is_closed = TRUE;
if (NULL != response_body) li_chunkqueue_skip_all(response_body);
if (!upgraded) raw_out->is_closed = TRUE;
}
/* Status line */
@ -97,7 +97,9 @@ void li_response_send_headers(liVRequest *vr, liChunkQueue *raw_out, liChunkQueu
}
/* connection header, if needed. connection entries in the list are ignored below, send them directly */
if (vr->request.http_version == LI_HTTP_VERSION_1_1) {
if (upgraded) {
g_string_append_len(head, CONST_STR_LEN("Connection: Upgrade\r\n"));
} else if (vr->request.http_version == LI_HTTP_VERSION_1_1) {
if (!vr->coninfo->keep_alive)
g_string_append_len(head, CONST_STR_LEN("Connection: close\r\n"));
} else {

53
src/main/stream_http_response.c

@ -11,9 +11,8 @@ struct liStreamHttpResponse {
liFilterChunkedDecodeState chunked_decode_state;
};
static gboolean check_response_header(liStreamHttpResponse* shr) {
static void check_response_header(liStreamHttpResponse* shr) {
liResponse *resp = &shr->vr->response;
liHttpHeader *hh;
GList *l;
shr->transfer_encoding_chunked = FALSE;
@ -22,7 +21,7 @@ static gboolean check_response_header(liStreamHttpResponse* shr) {
l = li_http_header_find_first(resp->headers, CONST_STR_LEN("transfer-encoding"));
if (l) {
for ( ; l ; l = li_http_header_find_next(l, CONST_STR_LEN("transfer-encoding")) ) {
hh = (liHttpHeader*) l->data;
liHttpHeader *hh = (liHttpHeader*) l->data;
if (0 == g_ascii_strcasecmp( LI_HEADER_VALUE(hh), "identity" )) {
/* ignore */
continue;
@ -30,13 +29,13 @@ static gboolean check_response_header(liStreamHttpResponse* shr) {
if (shr->transfer_encoding_chunked) {
VR_ERROR(shr->vr, "%s", "Response is chunked encoded twice");
li_vrequest_error(shr->vr);
return FALSE;
return;
}
shr->transfer_encoding_chunked = TRUE;
} else {
VR_ERROR(shr->vr, "Response has unsupported Transfer-Encoding: %s", LI_HEADER_VALUE(hh));
li_vrequest_error(shr->vr);
return FALSE;
return;
}
}
li_http_header_remove(resp->headers, CONST_STR_LEN("transfer-encoding"));
@ -46,10 +45,49 @@ static gboolean check_response_header(liStreamHttpResponse* shr) {
}
}
/* Upgrade: */
l = li_http_header_find_first(resp->headers, CONST_STR_LEN("upgrade"));
if (l) {
gboolean have_connection_upgrade = FALSE;
liHttpHeaderTokenizer header_tokenizer;
GString *token;
if (101 != resp->http_status) {
VR_ERROR(shr->vr, "Upgrade but status is %i instead of 101 'Switching Protocols'", resp->http_status);
li_vrequest_error(shr->vr);
return;
}
if (shr->transfer_encoding_chunked) {
VR_ERROR(shr->vr, "%s", "Upgrade with Transfer-Encoding: chunked");
li_vrequest_error(shr->vr);
return;
}
/* requires Connection: Upgrade header */
token = g_string_sized_new(15);
li_http_header_tokenizer_start(&header_tokenizer, resp->headers, CONST_STR_LEN("Connection"));
while (li_http_header_tokenizer_next(&header_tokenizer, token)) {
VR_ERROR(shr->vr, "Parsing header '%s'", ((liHttpHeader*)header_tokenizer.cur->data)->data->str);
VR_ERROR(shr->vr, "Connection token '%s'", token->str);
if (0 == g_ascii_strcasecmp(token->str, "Upgrade")) {
have_connection_upgrade = TRUE;
break;
}
}
g_string_free(token, TRUE); token = NULL;
if (!have_connection_upgrade) {
VR_ERROR(shr->vr, "%s", "Upgrade without Connection: Upgrade Transfer");
li_vrequest_error(shr->vr);
return;
}
shr->response_headers_finished = TRUE;
shr->vr->backend_drain->out->is_closed = FALSE;
li_vrequest_connection_upgrade(shr->vr, shr->vr->backend_drain, &shr->stream);
return;
}
shr->response_headers_finished = TRUE;
li_vrequest_indirect_headers_ready(shr->vr);
return TRUE;
return;
}
static void stream_http_response_data(liStreamHttpResponse* shr) {
@ -58,7 +96,8 @@ static void stream_http_response_data(liStreamHttpResponse* shr) {
if (!shr->response_headers_finished) {
switch (li_http_response_parse(shr->vr, &shr->parse_response_ctx)) {
case LI_HANDLER_GO_ON:
if (!check_response_header(shr)) return;
check_response_header(shr);
if (NULL == shr->stream.source) return;
break;
case LI_HANDLER_ERROR:
VR_ERROR(shr->vr, "%s", "Parsing response header failed");

8
src/main/subrequest_lua.c

@ -288,10 +288,16 @@ static liThrottleState* subvr_handle_throttle_in(liVRequest *vr) {
return NULL;
}
static void subvr_connection_upgrade(liVRequest *vr, liStream *backend_drain, liStream *backend_source) {
UNUSED(backend_drain); UNUSED(backend_source);
subvr_handle_response_error(vr);
}
static const liConCallbacks subrequest_callbacks = {
subvr_handle_response_error,
subvr_handle_throttle_out,
subvr_handle_throttle_in
subvr_handle_throttle_in,
subvr_connection_upgrade
};
static liSubrequest* subrequest_new(liVRequest *vr) {

16
src/main/virtualrequest.c

@ -399,7 +399,23 @@ void li_vrequest_indirect_headers_ready(liVRequest* vr) {
li_vrequest_joblist_append(vr);
}
void li_vrequest_connection_upgrade(liVRequest *vr, liStream *backend_drain, liStream *backend_source) {
assert(LI_VRS_HANDLE_RESPONSE_HEADERS > vr->state);
/* abort config handling. no filter, no more headers, ... */
vr->state = LI_VRS_WRITE_CONTENT;
li_action_stack_reset(vr, &vr->action_stack);
if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) {
VR_DEBUG(vr, "%s", "connection uprade");
}
/* we don't want these to be disconnected by a li_vrequest_reset */
li_stream_safe_release(&vr->backend_drain);
li_stream_safe_release(&vr->backend_source);
vr->coninfo->callbacks->connection_upgrade(vr, backend_drain, backend_source);
}
gboolean li_vrequest_is_handled(liVRequest *vr) {
return vr->state >= LI_VRS_READ_CONTENT;

4
src/modules/fastcgi_stream.c

@ -568,7 +568,9 @@ static void fastcgi_stream_out(liStream *stream, liStreamEvent event) {
li_stream_notify(stream);
break;
case LI_STREAM_CONNECTED_SOURCE:
assert(!ctx->stdin_closed);
/* support Connection: Upgrade by reopening stdin. not standard compliant,
* but the backend asked for it :) */
ctx->stdin_closed = FALSE;
break;
case LI_STREAM_DISCONNECTED_SOURCE:
if (!ctx->stdin_closed) {

10
src/modules/mod_proxy.c

@ -57,6 +57,8 @@ static void proxy_send_headers(liVRequest *vr, liChunkQueue *out) {
liHttpHeader *header;
GList *iter;
gchar *enc_path;
liHttpHeaderTokenizer header_tokenizer;
GString *tmp_str = vr->wrk->tmp_str;
g_string_append_len(head, GSTR_LEN(vr->request.http_method_str));
g_string_append_len(head, CONST_STR_LEN(" "));
@ -81,11 +83,19 @@ static void proxy_send_headers(liVRequest *vr, liChunkQueue *out) {
break;
}
li_http_header_tokenizer_start(&header_tokenizer, vr->request.headers, CONST_STR_LEN("Connection"));
while (li_http_header_tokenizer_next(&header_tokenizer, tmp_str)) {
if (0 == g_ascii_strcasecmp(tmp_str->str, "Upgrade")) {
g_string_append_len(head, CONST_STR_LEN("Connection: Upgrade\r\n"));
}
}
for (iter = g_queue_peek_head_link(&vr->request.headers->entries); iter; iter = g_list_next(iter)) {
header = (liHttpHeader*) iter->data;
if (li_http_header_key_is(header, CONST_STR_LEN("Connection"))) continue;
if (li_http_header_key_is(header, CONST_STR_LEN("Proxy-Connection"))) continue;
if (li_http_header_key_is(header, CONST_STR_LEN("X-Forwarded-Proto"))) continue;
if (li_http_header_key_is(header, CONST_STR_LEN("X-Forwarded-For"))) continue;
g_string_append_len(head, GSTR_LEN(header->data));
g_string_append_len(head, CONST_STR_LEN("\r\n"));
}

24
src/modules/mod_status.c

@ -56,7 +56,7 @@ static liHandlerResult status_info_runtime(liVRequest *vr, liPlugin *p);
static gint str_comp(gconstpointer a, gconstpointer b);
/* auto format constants */
static gchar liConnectionState_short[LI_CON_STATE_LAST+2] = "_cKqrhw";
static gchar liConnectionState_short[LI_CON_STATE_LAST+2] = "_cKqrhwu";
/* html snippet constants */
static const gchar html_header[] =
@ -176,10 +176,11 @@ static const gchar html_connections_sum[] =
" <tr>\n"
" <th style=\"width: 100px;\">inactive</th>\n"
" <th style=\"width: 100px;\">request start</th>\n"
" <th style=\"width: 150px;\">read request header</th>\n"
" <th style=\"width: 150px;\">handle request</th>\n"
" <th style=\"width: 150px;\">write response</th>\n"
" <th style=\"width: 150px;\">keep-alive</th>\n"
" <th style=\"width: 100px;\">read header</th>\n"
" <th style=\"width: 100px;\">handle request</th>\n"
" <th style=\"width: 100px;\">write response</th>\n"
" <th style=\"width: 100px;\">keep-alive</th>\n"
" <th style=\"width: 100px;\">upgraded</th>\n"
" </tr>\n"
" <tr>\n"
" <td>%u</td>\n"
@ -188,6 +189,7 @@ static const gchar html_connections_sum[] =
" <td>%u</td>\n"
" <td>%u</td>\n"
" <td>%u</td>\n"
" <td>%u</td>\n"
" </tr>\n"
" </table>\n";
static const gchar html_status_codes[] =
@ -737,9 +739,11 @@ static GString *status_info_full(liVRequest *vr, liPlugin *p, gboolean short_inf
/* connection counts */
g_string_append_len(html, CONST_STR_LEN("<div class=\"title\"><strong>Connections</strong> (states, sum)</div>\n"));
g_string_append_printf(html, html_connections_sum, connection_count[0] + connection_count[1],
connection_count[3], connection_count[4], connection_count[5], connection_count[6],
connection_count[2]
g_string_append_printf(html, html_connections_sum,
connection_count[LI_CON_STATE_DEAD] + connection_count[LI_CON_STATE_CLOSE],
connection_count[LI_CON_STATE_REQUEST_START], connection_count[LI_CON_STATE_READ_REQUEST_HEADER],
connection_count[LI_CON_STATE_HANDLE_MAINVR], connection_count[LI_CON_STATE_WRITE],
connection_count[LI_CON_STATE_KEEP_ALIVE], connection_count[LI_CON_STATE_UPGRADED]
);
/* response status codes */
@ -874,6 +878,8 @@ static GString *status_info_plain(liVRequest *vr, guint uptime, liStatistics *to
li_string_append_int(html, connection_count[LI_CON_STATE_WRITE]);
g_string_append_len(html, CONST_STR_LEN("\nconnection_state_keep_alive: "));
li_string_append_int(html, connection_count[LI_CON_STATE_KEEP_ALIVE]);
g_string_append_len(html, CONST_STR_LEN("\nconnection_state_upgraded: "));
li_string_append_int(html, connection_count[LI_CON_STATE_UPGRADED]);
/* status cpdes */
g_string_append_len(html, CONST_STR_LEN("\n\n# Status Codes (since start)\nstatus_1xx: "));
li_string_append_int(html, mod_status_response_codes[0]);
@ -906,7 +912,7 @@ static GString *status_info_auto(liVRequest *vr, guint uptime, liStatistics *tot
li_string_append_int(html, (gint64)uptime);
/* connection states */
g_string_append_len(html, CONST_STR_LEN("\nBusyServers: "));
li_string_append_int(html, connection_count[3]+connection_count[4]+connection_count[5]+connection_count[6]);
li_string_append_int(html, connection_count[3]+connection_count[4]+connection_count[5]+connection_count[6]+connection_count[7]);
g_string_append_len(html, CONST_STR_LEN("\nIdleServers: "));
li_string_append_int(html, connection_count[0]+connection_count[1]+connection_count[2]);
/* average since start */

Loading…
Cancel
Save