1 #include "first.h" 2 3 #include "base.h" 4 #include "log.h" 5 #include "buffer.h" 6 #include "http_header.h" 7 8 #include "plugin.h" 9 #include "stat_cache.h" 10 11 #include <stdlib.h> 12 #include <string.h> 13 #include <time.h> 14 15 /** 16 * this is a expire module for a lighttpd 17 * 18 * set 'Expires:' HTTP Headers on demand 19 */ 20 21 22 23 /* plugin config for all request/connections */ 24 25 typedef struct { 26 array *expire_url; 27 array *expire_mimetypes; 28 } plugin_config; 29 30 typedef struct { 31 PLUGIN_DATA; 32 33 buffer *expire_tstmp; 34 35 plugin_config **config_storage; 36 37 plugin_config conf; 38 } plugin_data; 39 40 /* init the plugin data */ 41 INIT_FUNC(mod_expire_init) { 42 plugin_data *p; 43 44 p = calloc(1, sizeof(*p)); 45 46 p->expire_tstmp = buffer_init(); 47 48 buffer_string_prepare_copy(p->expire_tstmp, 255); 49 50 return p; 51 } 52 53 /* detroy the plugin data */ 54 FREE_FUNC(mod_expire_free) { 55 plugin_data *p = p_d; 56 57 UNUSED(srv); 58 59 if (!p) return HANDLER_GO_ON; 60 61 buffer_free(p->expire_tstmp); 62 63 if (p->config_storage) { 64 size_t i; 65 for (i = 0; i < srv->config_context->used; i++) { 66 plugin_config *s = p->config_storage[i]; 67 68 if (NULL == s) continue; 69 70 array_free(s->expire_url); 71 array_free(s->expire_mimetypes); 72 free(s); 73 } 74 free(p->config_storage); 75 } 76 77 free(p); 78 79 return HANDLER_GO_ON; 80 } 81 82 static int mod_expire_get_offset(server *srv, plugin_data *p, buffer *expire, time_t *offset) { 83 char *ts; 84 int type = -1; 85 time_t retts = 0; 86 87 UNUSED(p); 88 89 /* 90 * parse 91 * 92 * '(access|now|modification) [plus] {<num> <type>}*' 93 * 94 * e.g. 'access 1 years' 95 */ 96 97 if (buffer_string_is_empty(expire)) { 98 log_error_write(srv, __FILE__, __LINE__, "s", 99 "empty:"); 100 return -1; 101 } 102 103 ts = expire->ptr; 104 105 if (0 == strncmp(ts, "access ", 7)) { 106 type = 0; 107 ts += 7; 108 } else if (0 == strncmp(ts, "now ", 4)) { 109 type = 0; 110 ts += 4; 111 } else if (0 == strncmp(ts, "modification ", 13)) { 112 type = 1; 113 ts += 13; 114 } else { 115 /* invalid type-prefix */ 116 log_error_write(srv, __FILE__, __LINE__, "ss", 117 "invalid <base>:", ts); 118 return -1; 119 } 120 121 if (0 == strncmp(ts, "plus ", 5)) { 122 /* skip the optional plus */ 123 ts += 5; 124 } 125 126 /* the rest is just <number> (years|months|weeks|days|hours|minutes|seconds) */ 127 while (1) { 128 char *space, *err; 129 int num; 130 131 if (NULL == (space = strchr(ts, ' '))) { 132 log_error_write(srv, __FILE__, __LINE__, "ss", 133 "missing space after <num>:", ts); 134 return -1; 135 } 136 137 num = strtol(ts, &err, 10); 138 if (*err != ' ') { 139 log_error_write(srv, __FILE__, __LINE__, "ss", 140 "missing <type> after <num>:", ts); 141 return -1; 142 } 143 144 ts = space + 1; 145 146 if (NULL != (space = strchr(ts, ' '))) { 147 int slen; 148 /* */ 149 150 slen = space - ts; 151 152 if (slen == 5 && 153 0 == strncmp(ts, "years", slen)) { 154 num *= 60 * 60 * 24 * 30 * 12; 155 } else if (slen == 6 && 156 0 == strncmp(ts, "months", slen)) { 157 num *= 60 * 60 * 24 * 30; 158 } else if (slen == 5 && 159 0 == strncmp(ts, "weeks", slen)) { 160 num *= 60 * 60 * 24 * 7; 161 } else if (slen == 4 && 162 0 == strncmp(ts, "days", slen)) { 163 num *= 60 * 60 * 24; 164 } else if (slen == 5 && 165 0 == strncmp(ts, "hours", slen)) { 166 num *= 60 * 60; 167 } else if (slen == 7 && 168 0 == strncmp(ts, "minutes", slen)) { 169 num *= 60; 170 } else if (slen == 7 && 171 0 == strncmp(ts, "seconds", slen)) { 172 num *= 1; 173 } else { 174 log_error_write(srv, __FILE__, __LINE__, "ss", 175 "unknown type:", ts); 176 return -1; 177 } 178 179 retts += num; 180 181 ts = space + 1; 182 } else { 183 if (0 == strcmp(ts, "years")) { 184 num *= 60 * 60 * 24 * 30 * 12; 185 } else if (0 == strcmp(ts, "months")) { 186 num *= 60 * 60 * 24 * 30; 187 } else if (0 == strcmp(ts, "weeks")) { 188 num *= 60 * 60 * 24 * 7; 189 } else if (0 == strcmp(ts, "days")) { 190 num *= 60 * 60 * 24; 191 } else if (0 == strcmp(ts, "hours")) { 192 num *= 60 * 60; 193 } else if (0 == strcmp(ts, "minutes")) { 194 num *= 60; 195 } else if (0 == strcmp(ts, "seconds")) { 196 num *= 1; 197 } else { 198 log_error_write(srv, __FILE__, __LINE__, "ss", 199 "unknown type:", ts); 200 return -1; 201 } 202 203 retts += num; 204 205 break; 206 } 207 } 208 209 if (offset != NULL) *offset = retts; 210 211 return type; 212 } 213 214 215 /* handle plugin config and check values */ 216 217 SETDEFAULTS_FUNC(mod_expire_set_defaults) { 218 plugin_data *p = p_d; 219 size_t i = 0, k; 220 221 config_values_t cv[] = { 222 { "expire.url", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION }, /* 0 */ 223 { "expire.mimetypes", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION }, /* 1 */ 224 { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET } 225 }; 226 227 if (!p) return HANDLER_ERROR; 228 229 p->config_storage = calloc(srv->config_context->used, sizeof(plugin_config *)); 230 231 for (i = 0; i < srv->config_context->used; i++) { 232 data_config const* config = (data_config const*)srv->config_context->data[i]; 233 plugin_config *s; 234 235 s = calloc(1, sizeof(plugin_config)); 236 s->expire_url = array_init(); 237 s->expire_mimetypes = array_init(); 238 239 cv[0].destination = s->expire_url; 240 cv[1].destination = s->expire_mimetypes; 241 242 p->config_storage[i] = s; 243 244 if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) { 245 return HANDLER_ERROR; 246 } 247 248 if (!array_is_kvstring(s->expire_url)) { 249 log_error_write(srv, __FILE__, __LINE__, "s", 250 "unexpected value for expire.url; expected list of \"urlpath\" => \"expiration\""); 251 return HANDLER_ERROR; 252 } 253 254 for (k = 0; k < s->expire_url->used; k++) { 255 data_string *ds = (data_string *)s->expire_url->data[k]; 256 257 /* parse lines */ 258 if (-1 == mod_expire_get_offset(srv, p, ds->value, NULL)) { 259 log_error_write(srv, __FILE__, __LINE__, "sb", 260 "parsing expire.url failed:", ds->value); 261 return HANDLER_ERROR; 262 } 263 } 264 265 if (!array_is_kvstring(s->expire_mimetypes)) { 266 log_error_write(srv, __FILE__, __LINE__, "s", 267 "unexpected value for expire.mimetypes; expected list of \"mimetype\" => \"expiration\""); 268 return HANDLER_ERROR; 269 } 270 271 for (k = 0; k < s->expire_mimetypes->used; k++) { 272 data_string *ds = (data_string *)s->expire_mimetypes->data[k]; 273 size_t klen = buffer_string_length(ds->key); 274 275 /*(omit trailing '*', if present, from prefix match)*/ 276 /*(not usually a good idea to modify array keys 277 * since doing so might break array_get_element_klen() search, 278 * but array use in this module only walks array)*/ 279 if (klen && ds->key->ptr[klen-1] == '*') buffer_string_set_length(ds->key, klen-1); 280 281 /* parse lines */ 282 if (-1 == mod_expire_get_offset(srv, p, ds->value, NULL)) { 283 log_error_write(srv, __FILE__, __LINE__, "sb", 284 "parsing expire.mimetypes failed:", ds->value); 285 return HANDLER_ERROR; 286 } 287 } 288 } 289 290 291 return HANDLER_GO_ON; 292 } 293 294 #define PATCH(x) \ 295 p->conf.x = s->x; 296 static int mod_expire_patch_connection(server *srv, connection *con, plugin_data *p) { 297 size_t i, j; 298 plugin_config *s = p->config_storage[0]; 299 300 PATCH(expire_url); 301 PATCH(expire_mimetypes); 302 303 /* skip the first, the global context */ 304 for (i = 1; i < srv->config_context->used; i++) { 305 data_config *dc = (data_config *)srv->config_context->data[i]; 306 s = p->config_storage[i]; 307 308 /* condition didn't match */ 309 if (!config_check_cond(srv, con, dc)) continue; 310 311 /* merge config */ 312 for (j = 0; j < dc->value->used; j++) { 313 data_unset *du = dc->value->data[j]; 314 315 if (buffer_is_equal_string(du->key, CONST_STR_LEN("expire.url"))) { 316 PATCH(expire_url); 317 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("expire.mimetypes"))) { 318 PATCH(expire_mimetypes); 319 } 320 } 321 } 322 323 return 0; 324 } 325 #undef PATCH 326 327 CONNECTION_FUNC(mod_expire_handler) { 328 plugin_data *p = p_d; 329 buffer *vb; 330 data_string *ds; 331 332 /* Add caching headers only to http_status 200 OK or 206 Partial Content */ 333 if (con->http_status != 200 && con->http_status != 206) return HANDLER_GO_ON; 334 /* Add caching headers only to GET or HEAD requests */ 335 if ( con->request.http_method != HTTP_METHOD_GET 336 && con->request.http_method != HTTP_METHOD_HEAD) return HANDLER_GO_ON; 337 /* Add caching headers only if not already present */ 338 vb = http_header_response_get(con, HTTP_HEADER_CACHE_CONTROL, CONST_STR_LEN("Cache-Control")); 339 if (NULL != vb) return HANDLER_GO_ON; 340 341 if (buffer_is_empty(con->uri.path)) return HANDLER_GO_ON; 342 343 mod_expire_patch_connection(srv, con, p); 344 345 /* check expire.url */ 346 ds = (data_string *)array_match_key_prefix(p->conf.expire_url, con->uri.path); 347 if (NULL != ds) { 348 vb = ds->value; 349 } 350 else { 351 /* check expire.mimetypes (if no match with expire.url) */ 352 vb = http_header_response_get(con, HTTP_HEADER_CONTENT_TYPE, CONST_STR_LEN("Content-Type")); 353 ds = (NULL != vb) 354 ? (data_string *)array_match_key_prefix(p->conf.expire_mimetypes, vb) 355 : (data_string *)array_get_element_klen(p->conf.expire_mimetypes, CONST_STR_LEN("")); 356 if (NULL == ds) return HANDLER_GO_ON; 357 vb = ds->value; 358 } 359 360 if (NULL != vb) { 361 time_t ts, expires; 362 stat_cache_entry *sce = NULL; 363 364 switch(mod_expire_get_offset(srv, p, vb, &ts)) { 365 case 0: 366 /* access */ 367 expires = (ts + srv->cur_ts); 368 break; 369 case 1: 370 /* modification */ 371 372 /* if stat fails => sce == NULL, ignore return value */ 373 (void) stat_cache_get_entry(srv, con, con->physical.path, &sce); 374 375 /* can't set modification based expire header if 376 * mtime is not available 377 */ 378 if (NULL == sce) return HANDLER_GO_ON; 379 380 expires = (ts + sce->st.st_mtime); 381 break; 382 default: 383 /* -1 is handled at parse-time */ 384 return HANDLER_ERROR; 385 } 386 387 /* expires should be at least srv->cur_ts */ 388 if (expires < srv->cur_ts) expires = srv->cur_ts; 389 390 buffer_clear(p->expire_tstmp); 391 buffer_append_strftime(p->expire_tstmp, "%a, %d %b %Y %H:%M:%S GMT", gmtime(&(expires))); 392 393 /* HTTP/1.0 */ 394 http_header_response_set(con, HTTP_HEADER_OTHER, CONST_STR_LEN("Expires"), CONST_BUF_LEN(p->expire_tstmp)); 395 396 /* HTTP/1.1 */ 397 buffer_copy_string_len(p->expire_tstmp, CONST_STR_LEN("max-age=")); 398 buffer_append_int(p->expire_tstmp, expires - srv->cur_ts); /* as expires >= srv->cur_ts the difference is >= 0 */ 399 400 http_header_response_set(con, HTTP_HEADER_CACHE_CONTROL, CONST_STR_LEN("Cache-Control"), CONST_BUF_LEN(p->expire_tstmp)); 401 402 return HANDLER_GO_ON; 403 } 404 405 /* not found */ 406 return HANDLER_GO_ON; 407 } 408 409 /* this function is called at dlopen() time and inits the callbacks */ 410 411 int mod_expire_plugin_init(plugin *p); 412 int mod_expire_plugin_init(plugin *p) { 413 p->version = LIGHTTPD_VERSION_ID; 414 p->name = buffer_init_string("expire"); 415 416 p->init = mod_expire_init; 417 p->handle_response_start = mod_expire_handler; 418 p->set_defaults = mod_expire_set_defaults; 419 p->cleanup = mod_expire_free; 420 421 p->data = NULL; 422 423 return 0; 424 } 425