#include #include #include #include "base.h" #include "log.h" #include "buffer.h" #include "plugin.h" #include "response.h" #include "stat_cache.h" #define CONFIG_UPLOAD_PROGRESS_URL "upload-progress.progress-url" #define CONFIG_UPLOAD_PROGRESS_TIMEOUT "upload-progress.remove-timeout" #define CONFIG_UPLOAD_PROGRESS_DEBUG "upload-progress.debug" /** * uploadprogress for lighttpd * * Initial: Jan Kneschke * Timeout+Status addon: Bjoern Kalkbrenner [20070112] * * the timeout is used to keep in the status information intact even if the parent * connection is gone already */ typedef struct { buffer *tracking_id; connection *con; time_t timeout; int status; off_t size; } connection_map_entry; typedef struct { connection_map_entry **ptr; size_t used; size_t size; } connection_map; /* plugin config for all request/connections */ typedef struct { buffer *progress_url; unsigned short debug; unsigned short remove_timeout; } plugin_config; typedef struct { PLUGIN_DATA; connection_map *con_map; buffer *tmp_buf; /** used as temporary buffer for extracting the tracking id */ plugin_config **config_storage; plugin_config conf; } plugin_data; /** * * connection maps * */ /* init the plugin data */ static connection_map *connection_map_init() { connection_map *cm; cm = calloc(1, sizeof(*cm)); return cm; } static void connection_map_free(connection_map *cm) { size_t i; for (i = 0; i < cm->size; i++) { connection_map_entry *cme = cm->ptr[i]; if (!cme) break; if (cme->tracking_id) { buffer_free(cme->tracking_id); } free(cme); } free(cm); } static connection_map_entry *connection_map_insert(connection_map *cm, buffer *tracking_id, connection *con) { connection_map_entry *cme; size_t i; if (cm->size == 0) { cm->size = 16; cm->ptr = malloc(cm->size * sizeof(*(cm->ptr))); for (i = 0; i < cm->size; i++) { cm->ptr[i] = NULL; } } else if (cm->used == cm->size) { cm->size += 16; cm->ptr = realloc(cm->ptr, cm->size * sizeof(*(cm->ptr))); for (i = cm->used; i < cm->size; i++) { cm->ptr[i] = NULL; } } if (cm->ptr[cm->used]) { /* is already alloced, just reuse it */ cme = cm->ptr[cm->used]; } else { cme = malloc(sizeof(*cme)); cme->tracking_id = buffer_init(); } cme->timeout = 0; cme->status = 0; buffer_copy_string_buffer(cme->tracking_id, tracking_id); cme->con = con; cm->ptr[cm->used++] = cme; return cme; } static connection_map_entry *connection_map_get_connection_entry(connection_map *cm, buffer *tracking_id) { size_t i; for (i = 0; i < cm->used; i++) { connection_map_entry *cme = cm->ptr[i]; if (buffer_is_equal(cme->tracking_id, tracking_id)) { /* found connection */ return cme; } } return NULL; } static void connection_map_remove_connection(connection_map *cm, size_t i) { connection_map_entry *cme = cm->ptr[i]; buffer_reset(cme->tracking_id); cme->timeout=0; cme->status=0; cm->used--; /* swap positions with the last entry */ if (cm->used) { cm->ptr[i] = cm->ptr[cm->used]; cm->ptr[cm->used] = cme; } } /** * remove dead tracking IDs * * uploadprogress.remove-timeout sets a grace-period in which the * connection status is still known even of the connection is already * being removed * */ static void connection_map_clear_timeout_connections(connection_map *cm) { size_t i; time_t now_t = time(NULL); for (i = 0; i < cm->used; i++) { connection_map_entry *cme = cm->ptr[i]; if (cme->timeout != 0 && cme->timeout < now_t) { /* found connection */ connection_map_remove_connection(cm, i); } } } /** * extract the tracking-id from the parameters * * for POST requests it is part of the request headers * for GET requests ... too */ static buffer *get_tracking_id(plugin_data *p, connection *con) { data_string *ds; buffer *b = NULL; char *qstr=NULL; size_t i; /* the request has to contain a 32byte ID */ if (NULL == (ds = (data_string *)array_get_element(con->request.headers, CONST_STR_LEN("X-Progress-ID")))) { char *amp = NULL; /* perhaps the POST request is using the querystring to pass the X-Progress-ID */ if (buffer_is_empty(con->uri.query)) { /* * con->uri.query will not be parsed out if a 413 error happens */ if (NULL != (qstr = strchr(con->request.uri->ptr, '?'))) { /** extract query string from request.uri */ buffer_copy_string(con->uri.query, qstr + 1); } else { return NULL; } } /** split the query-string and extract the X-Progress-ID */ do { char *eq = NULL; char *start = amp ? amp + 1 : con->uri.query->ptr; amp = strchr(start, '&'); /* check the string between start and amp for = */ if (amp) { buffer_copy_string_len(p->tmp_buf, start, amp - start); } else { buffer_copy_string(p->tmp_buf, start); } eq = strchr(p->tmp_buf->ptr, '='); if (eq) { *eq = '\0'; if (0 == strcmp(p->tmp_buf->ptr, "X-Progress-ID")) { size_t key_len = sizeof("X-Progress-ID") - 1; size_t var_len = p->tmp_buf->used - 1; /* found */ buffer_copy_string_len(p->tmp_buf, start + key_len + 1, var_len - key_len - 1); b = p->tmp_buf; break; } } } while (amp); if (!b) return NULL; } else { /* request header was found, use it */ b = ds->value; } if (b->used != 32 + 1) { if (p->conf.debug) ERROR("the Progress-ID has to be 32 characters long, got %zd characters", b->used - 1); return NULL; } for (i = 0; i < b->used - 1; i++) { char c = b->ptr[i]; if (!light_isxdigit(c)) { if (p->conf.debug) ERROR("only hex-digits are allowed (0-9 + a-f): (ascii: %d)", c); return NULL; } } return b; } /* init the plugin data */ INIT_FUNC(mod_uploadprogress_init) { plugin_data *p; UNUSED(srv); p = calloc(1, sizeof(*p)); p->con_map = connection_map_init(); p->tmp_buf = buffer_init(); return p; } /* detroy the plugin data */ FREE_FUNC(mod_uploadprogress_free) { plugin_data *p = p_d; if (!p) return HANDLER_GO_ON; if (p->config_storage) { size_t i; for (i = 0; i < srv->config_context->used; i++) { plugin_config *s = p->config_storage[i]; buffer_free(s->progress_url); s->remove_timeout=0; free(s); } free(p->config_storage); } connection_map_free(p->con_map); buffer_free(p->tmp_buf); free(p); return HANDLER_GO_ON; } /* handle plugin config and check values */ SETDEFAULTS_FUNC(mod_uploadprogress_set_defaults) { plugin_data *p = p_d; size_t i = 0; config_values_t cv[] = { { CONFIG_UPLOAD_PROGRESS_URL, NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 0 */ { CONFIG_UPLOAD_PROGRESS_TIMEOUT, NULL, T_CONFIG_SHORT, T_CONFIG_SCOPE_CONNECTION }, /* 1 */ { CONFIG_UPLOAD_PROGRESS_DEBUG, NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 2 */ { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET } }; if (!p) return HANDLER_ERROR; p->config_storage = calloc(1, srv->config_context->used * sizeof(specific_config *)); for (i = 0; i < srv->config_context->used; i++) { plugin_config *s; s = calloc(1, sizeof(plugin_config)); s->progress_url = buffer_init(); s->remove_timeout = 60; s->debug = 0; cv[0].destination = s->progress_url; cv[1].destination = &(s->remove_timeout); cv[2].destination = &(s->debug); p->config_storage[i] = s; if (0 != config_insert_values_global(srv, ((data_config *)srv->config_context->data[i])->value, cv)) { return HANDLER_ERROR; } } return HANDLER_GO_ON; } static int mod_uploadprogress_patch_connection(server *srv, connection *con, plugin_data *p) { size_t i, j; plugin_config *s = p->config_storage[0]; PATCH_OPTION(progress_url); PATCH_OPTION(remove_timeout); PATCH_OPTION(debug); /* skip the first, the global context */ for (i = 1; i < srv->config_context->used; i++) { data_config *dc = (data_config *)srv->config_context->data[i]; s = p->config_storage[i]; /* condition didn't match */ if (!config_check_cond(srv, con, dc)) continue; /* merge config */ for (j = 0; j < dc->value->used; j++) { data_unset *du = dc->value->data[j]; if (buffer_is_equal_string(du->key, CONST_STR_LEN(CONFIG_UPLOAD_PROGRESS_URL))) { PATCH_OPTION(progress_url); } else if (buffer_is_equal_string(du->key, CONST_STR_LEN(CONFIG_UPLOAD_PROGRESS_TIMEOUT))) { PATCH_OPTION(remove_timeout); } else if (buffer_is_equal_string(du->key, CONST_STR_LEN(CONFIG_UPLOAD_PROGRESS_DEBUG))) { PATCH_OPTION(debug); } } } return 0; } /** * * the idea: * * for the first request we check if it is a post-request * * if no, move out, don't care about them * * if yes, take the connection structure and register it locally * in the progress-struct together with an session-id (md5 ... ) * * if the connections closes, cleanup the entry in the progress-struct * * a second request can now get the info about the size of the upload, * the received bytes * */ URIHANDLER_FUNC(mod_uploadprogress_uri_handler) { plugin_data *p = p_d; buffer *tracking_id; buffer *b; connection_map_entry *post_con_entry = NULL; connection_map_entry *map_con_entry = NULL; if (buffer_is_empty(con->uri.path)) return HANDLER_GO_ON; mod_uploadprogress_patch_connection(srv, con, p); /* no progress URL set, ignore request */ if (buffer_is_empty(p->conf.progress_url)) return HANDLER_GO_ON; switch(con->request.http_method) { case HTTP_METHOD_POST: /** * a POST request is the UPLOAD itself * * get the unique tracker id */ if (NULL == (tracking_id = get_tracking_id(p, con))) { return HANDLER_GO_ON; } if (NULL == (map_con_entry = connection_map_get_connection_entry(p->con_map, tracking_id))) { connection_map_insert(p->con_map, tracking_id, con); if (p->conf.debug) TRACE("POST: connection is new, registered: %s", SAFE_BUF_STR(tracking_id)); } else { map_con_entry->timeout = 0; map_con_entry->status = 0; if (p->conf.debug) TRACE("POST: connection is known, id: %s", SAFE_BUF_STR(tracking_id)); } return HANDLER_GO_ON; case HTTP_METHOD_GET: /** * the status request for the current connection */ if (p->conf.debug) TRACE("(uploadprogress) urls %s == %s", SAFE_BUF_STR(con->uri.path), SAFE_BUF_STR(p->conf.progress_url)); if (!buffer_is_equal(con->uri.path, p->conf.progress_url)) { return HANDLER_GO_ON; } /* get the tracker id */ if (NULL == (tracking_id = get_tracking_id(p, con))) { return HANDLER_GO_ON; } buffer_reset(con->physical.path); con->file_started = 1; con->http_status = 200; con->send->is_closed = 1; /* send JSON content */ response_header_overwrite(srv, con, CONST_STR_LEN("Content-Type"), CONST_STR_LEN("text/javascript")); /* just an attempt the force the IE/proxies to NOT cache the request */ response_header_overwrite(srv, con, CONST_STR_LEN("Pragma"), CONST_STR_LEN("no-cache")); response_header_overwrite(srv, con, CONST_STR_LEN("Expires"), CONST_STR_LEN("Thu, 19 Nov 1981 08:52:00 GMT")); response_header_overwrite(srv, con, CONST_STR_LEN("Cache-Control"), CONST_STR_LEN("no-store, no-cache, must-revalidate, post-check=0, pre-check=0")); b = chunkqueue_get_append_buffer(con->send); /* get the connection */ if (NULL == (post_con_entry = connection_map_get_connection_entry(p->con_map, tracking_id))) { /** * looks like we don't know the tracking id yet, GET and POST out of sync ? */ buffer_append_string_len(b, CONST_STR_LEN("new Object({ 'state' : 'starting' })\r\n")); con->send->bytes_in += b->used-1; if (p->conf.debug) TRACE("connection unknown: %s, sending: %s", SAFE_BUF_STR(tracking_id), SAFE_BUF_STR(b)); return HANDLER_FINISHED; } buffer_copy_string_len(b, CONST_STR_LEN("new Object({ 'state' : ")); if (post_con_entry->status == 413) { /* the upload was too large */ buffer_append_string_len(b, CONST_STR_LEN("'error', 'status' : 413")); } else if (post_con_entry->con == NULL) { /* the connection is already gone */ buffer_append_string_len(b, CONST_STR_LEN("'done', 'size' : ")); buffer_append_off_t(b, post_con_entry->size); } else { /* the upload is already done, but the connection might be still open */ buffer_append_string(b, post_con_entry->con->recv->is_closed ? "'done'" : "'uploading'"); buffer_append_string_len(b, CONST_STR_LEN(", 'received' : ")); buffer_append_off_t(b, post_con_entry->con->recv->bytes_in); buffer_append_string_len(b, CONST_STR_LEN(", 'size' : ")); buffer_append_off_t(b, post_con_entry->con->request.content_length == -1 ? 0 : post_con_entry->con->request.content_length); } buffer_append_string_len(b, CONST_STR_LEN("})\r\n")); con->send->bytes_in += b->used-1; if (p->conf.debug) TRACE("connection is known: %s, sending: %s", SAFE_BUF_STR(tracking_id), SAFE_BUF_STR(b)); return HANDLER_FINISHED; default: break; } return HANDLER_GO_ON; } /** * check if request parser sent 413 for our POST request */ URIHANDLER_FUNC(mod_uploadprogress_response_header) { plugin_data *p = p_d; buffer *tracking_id; connection_map_entry *map_con_entry = NULL; UNUSED(srv); /* * we only want to process an 413 (Bad length) error for the upload (POST request) */ if (con->request.http_method != HTTP_METHOD_POST || con->http_status != 413) { return HANDLER_GO_ON; } if (p->conf.debug) { TRACE("response_header: con=%p, http_method=%d, http_status=%d", (void*) con, con->request.http_method, con->http_status); } /* get the tracker id */ if (NULL == (tracking_id = get_tracking_id(p, con))) { return HANDLER_GO_ON; } if (NULL == (map_con_entry = connection_map_get_connection_entry(p->con_map, tracking_id))) { /** * in case the request parser meant the request was too large the URI handler won't * get called. Insert the connection mapping here */ if (NULL == (map_con_entry = connection_map_insert(p->con_map, tracking_id, con))) { return HANDLER_GO_ON; } } /* ok, found our entries, setting 413 here for status */ map_con_entry->status = 413; return HANDLER_GO_ON; } /** * remove the parent connection from the connection mapping * when it got closed * * keep the mapping active for a while to send a valid final status */ REQUESTDONE_FUNC(mod_uploadprogress_request_done) { plugin_data *p = p_d; buffer *tracking_id; connection_map_entry *cm = NULL; UNUSED(srv); if (buffer_is_empty(con->uri.path)) return HANDLER_GO_ON; /* * only need to handle the upload request. */ if (con->request.http_method != HTTP_METHOD_POST) { return HANDLER_GO_ON; } if (NULL == (tracking_id = get_tracking_id(p, con))) { return HANDLER_GO_ON; } if (p->conf.debug) { TRACE("upload is done, moving tracking-id to backlog: tracking-id=%s, http_status=%d", SAFE_BUF_STR(tracking_id), con->http_status); } /* * set timeout on the upload's connection_map_entry. */ if (NULL == (cm = connection_map_get_connection_entry(p->con_map, tracking_id))) { if (p->conf.debug) { TRACE("tracking ID %s not found, can't set timeout", SAFE_BUF_STR(tracking_id)); } return HANDLER_GO_ON; } /* save request size to be able to report it even when cm->con == NULL */ cm->size = con->request.content_length; cm->timeout = time(NULL) + p->conf.remove_timeout; cm->con = NULL; /* con becomes invalid very soon */ return HANDLER_GO_ON; } /** * remove dead connections once in while */ TRIGGER_FUNC(mod_uploadprogress_trigger) { plugin_data *p = p_d; if ((srv->cur_ts % 10) != 0) return HANDLER_GO_ON; connection_map_clear_timeout_connections(p->con_map); return HANDLER_GO_ON; } /* this function is called at dlopen() time and inits the callbacks */ LI_EXPORT int mod_uploadprogress_plugin_init(plugin *p); LI_EXPORT int mod_uploadprogress_plugin_init(plugin *p) { p->version = LIGHTTPD_VERSION_ID; p->name = buffer_init_string("uploadprogress"); p->init = mod_uploadprogress_init; p->handle_uri_clean = mod_uploadprogress_uri_handler; p->handle_response_done = mod_uploadprogress_request_done; p->set_defaults = mod_uploadprogress_set_defaults; p->cleanup = mod_uploadprogress_free; p->handle_trigger = mod_uploadprogress_trigger; p->handle_response_header = mod_uploadprogress_response_header; p->data = NULL; return 0; }