1
2/*
3 * Copyright (C) Igor Sysoev
4 * Copyright (C) Nginx, Inc.
5 */
6
7
8#include <ngx_config.h>
9#include <ngx_core.h>
10#include <ngx_http.h>
11
12
13typedef struct ngx_http_header_val_s  ngx_http_header_val_t;
14
15typedef ngx_int_t (*ngx_http_set_header_pt)(ngx_http_request_t *r,
16    ngx_http_header_val_t *hv, ngx_str_t *value);
17
18
19typedef struct {
20    ngx_str_t                  name;
21    ngx_uint_t                 offset;
22    ngx_http_set_header_pt     handler;
23} ngx_http_set_header_t;
24
25
26struct ngx_http_header_val_s {
27    ngx_http_complex_value_t   value;
28    ngx_str_t                  key;
29    ngx_http_set_header_pt     handler;
30    ngx_uint_t                 offset;
31    ngx_uint_t                 always;  /* unsigned  always:1 */
32};
33
34
35typedef enum {
36    NGX_HTTP_EXPIRES_OFF,
37    NGX_HTTP_EXPIRES_EPOCH,
38    NGX_HTTP_EXPIRES_MAX,
39    NGX_HTTP_EXPIRES_ACCESS,
40    NGX_HTTP_EXPIRES_MODIFIED,
41    NGX_HTTP_EXPIRES_DAILY,
42    NGX_HTTP_EXPIRES_UNSET
43} ngx_http_expires_t;
44
45
46typedef struct {
47    ngx_http_expires_t         expires;
48    time_t                     expires_time;
49    ngx_http_complex_value_t  *expires_value;
50    ngx_array_t               *headers;
51} ngx_http_headers_conf_t;
52
53
54static ngx_int_t ngx_http_set_expires(ngx_http_request_t *r,
55    ngx_http_headers_conf_t *conf);
56static ngx_int_t ngx_http_parse_expires(ngx_str_t *value,
57    ngx_http_expires_t *expires, time_t *expires_time, char **err);
58static ngx_int_t ngx_http_add_cache_control(ngx_http_request_t *r,
59    ngx_http_header_val_t *hv, ngx_str_t *value);
60static ngx_int_t ngx_http_add_header(ngx_http_request_t *r,
61    ngx_http_header_val_t *hv, ngx_str_t *value);
62static ngx_int_t ngx_http_set_last_modified(ngx_http_request_t *r,
63    ngx_http_header_val_t *hv, ngx_str_t *value);
64static ngx_int_t ngx_http_set_response_header(ngx_http_request_t *r,
65    ngx_http_header_val_t *hv, ngx_str_t *value);
66
67static void *ngx_http_headers_create_conf(ngx_conf_t *cf);
68static char *ngx_http_headers_merge_conf(ngx_conf_t *cf,
69    void *parent, void *child);
70static ngx_int_t ngx_http_headers_filter_init(ngx_conf_t *cf);
71static char *ngx_http_headers_expires(ngx_conf_t *cf, ngx_command_t *cmd,
72    void *conf);
73static char *ngx_http_headers_add(ngx_conf_t *cf, ngx_command_t *cmd,
74    void *conf);
75
76
77static ngx_http_set_header_t  ngx_http_set_headers[] = {
78
79    { ngx_string("Cache-Control"), 0, ngx_http_add_cache_control },
80
81    { ngx_string("Last-Modified"),
82                 offsetof(ngx_http_headers_out_t, last_modified),
83                 ngx_http_set_last_modified },
84
85    { ngx_string("ETag"),
86                 offsetof(ngx_http_headers_out_t, etag),
87                 ngx_http_set_response_header },
88
89    { ngx_null_string, 0, NULL }
90};
91
92
93static ngx_command_t  ngx_http_headers_filter_commands[] = {
94
95    { ngx_string("expires"),
96      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF
97                        |NGX_CONF_TAKE12,
98      ngx_http_headers_expires,
99      NGX_HTTP_LOC_CONF_OFFSET,
100      0,
101      NULL},
102
103    { ngx_string("add_header"),
104      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF
105                        |NGX_CONF_TAKE23,
106      ngx_http_headers_add,
107      NGX_HTTP_LOC_CONF_OFFSET,
108      0,
109      NULL},
110
111      ngx_null_command
112};
113
114
115static ngx_http_module_t  ngx_http_headers_filter_module_ctx = {
116    NULL,                                  /* preconfiguration */
117    ngx_http_headers_filter_init,          /* postconfiguration */
118
119    NULL,                                  /* create main configuration */
120    NULL,                                  /* init main configuration */
121
122    NULL,                                  /* create server configuration */
123    NULL,                                  /* merge server configuration */
124
125    ngx_http_headers_create_conf,          /* create location configuration */
126    ngx_http_headers_merge_conf            /* merge location configuration */
127};
128
129
130ngx_module_t  ngx_http_headers_filter_module = {
131    NGX_MODULE_V1,
132    &ngx_http_headers_filter_module_ctx,   /* module context */
133    ngx_http_headers_filter_commands,      /* module directives */
134    NGX_HTTP_MODULE,                       /* module type */
135    NULL,                                  /* init master */
136    NULL,                                  /* init module */
137    NULL,                                  /* init process */
138    NULL,                                  /* init thread */
139    NULL,                                  /* exit thread */
140    NULL,                                  /* exit process */
141    NULL,                                  /* exit master */
142    NGX_MODULE_V1_PADDING
143};
144
145
146static ngx_http_output_header_filter_pt  ngx_http_next_header_filter;
147
148
149static ngx_int_t
150ngx_http_headers_filter(ngx_http_request_t *r)
151{
152    ngx_str_t                 value;
153    ngx_uint_t                i, safe_status;
154    ngx_http_header_val_t    *h;
155    ngx_http_headers_conf_t  *conf;
156
157    conf = ngx_http_get_module_loc_conf(r, ngx_http_headers_filter_module);
158
159    if ((conf->expires == NGX_HTTP_EXPIRES_OFF && conf->headers == NULL)
160        || r != r->main)
161    {
162        return ngx_http_next_header_filter(r);
163    }
164
165    switch (r->headers_out.status) {
166
167    case NGX_HTTP_OK:
168    case NGX_HTTP_CREATED:
169    case NGX_HTTP_NO_CONTENT:
170    case NGX_HTTP_PARTIAL_CONTENT:
171    case NGX_HTTP_MOVED_PERMANENTLY:
172    case NGX_HTTP_MOVED_TEMPORARILY:
173    case NGX_HTTP_SEE_OTHER:
174    case NGX_HTTP_NOT_MODIFIED:
175    case NGX_HTTP_TEMPORARY_REDIRECT:
176        safe_status = 1;
177        break;
178
179    default:
180        safe_status = 0;
181        break;
182    }
183
184    if (conf->expires != NGX_HTTP_EXPIRES_OFF && safe_status) {
185        if (ngx_http_set_expires(r, conf) != NGX_OK) {
186            return NGX_ERROR;
187        }
188    }
189
190    if (conf->headers) {
191        h = conf->headers->elts;
192        for (i = 0; i < conf->headers->nelts; i++) {
193
194            if (!safe_status && !h[i].always) {
195                continue;
196            }
197
198            if (ngx_http_complex_value(r, &h[i].value, &value) != NGX_OK) {
199                return NGX_ERROR;
200            }
201
202            if (h[i].handler(r, &h[i], &value) != NGX_OK) {
203                return NGX_ERROR;
204            }
205        }
206    }
207
208    return ngx_http_next_header_filter(r);
209}
210
211
212static ngx_int_t
213ngx_http_set_expires(ngx_http_request_t *r, ngx_http_headers_conf_t *conf)
214{
215    char                *err;
216    size_t               len;
217    time_t               now, expires_time, max_age;
218    ngx_str_t            value;
219    ngx_int_t            rc;
220    ngx_uint_t           i;
221    ngx_table_elt_t     *e, *cc, **ccp;
222    ngx_http_expires_t   expires;
223
224    expires = conf->expires;
225    expires_time = conf->expires_time;
226
227    if (conf->expires_value != NULL) {
228
229        if (ngx_http_complex_value(r, conf->expires_value, &value) != NGX_OK) {
230            return NGX_ERROR;
231        }
232
233        rc = ngx_http_parse_expires(&value, &expires, &expires_time, &err);
234
235        if (rc != NGX_OK) {
236            return NGX_OK;
237        }
238
239        if (expires == NGX_HTTP_EXPIRES_OFF) {
240            return NGX_OK;
241        }
242    }
243
244    e = r->headers_out.expires;
245
246    if (e == NULL) {
247
248        e = ngx_list_push(&r->headers_out.headers);
249        if (e == NULL) {
250            return NGX_ERROR;
251        }
252
253        r->headers_out.expires = e;
254
255        e->hash = 1;
256        ngx_str_set(&e->key, "Expires");
257    }
258
259    len = sizeof("Mon, 28 Sep 1970 06:00:00 GMT");
260    e->value.len = len - 1;
261
262    ccp = r->headers_out.cache_control.elts;
263
264    if (ccp == NULL) {
265
266        if (ngx_array_init(&r->headers_out.cache_control, r->pool,
267                           1, sizeof(ngx_table_elt_t *))
268            != NGX_OK)
269        {
270            return NGX_ERROR;
271        }
272
273        ccp = ngx_array_push(&r->headers_out.cache_control);
274        if (ccp == NULL) {
275            return NGX_ERROR;
276        }
277
278        cc = ngx_list_push(&r->headers_out.headers);
279        if (cc == NULL) {
280            return NGX_ERROR;
281        }
282
283        cc->hash = 1;
284        ngx_str_set(&cc->key, "Cache-Control");
285        *ccp = cc;
286
287    } else {
288        for (i = 1; i < r->headers_out.cache_control.nelts; i++) {
289            ccp[i]->hash = 0;
290        }
291
292        cc = ccp[0];
293    }
294
295    if (expires == NGX_HTTP_EXPIRES_EPOCH) {
296        e->value.data = (u_char *) "Thu, 01 Jan 1970 00:00:01 GMT";
297        ngx_str_set(&cc->value, "no-cache");
298        return NGX_OK;
299    }
300
301    if (expires == NGX_HTTP_EXPIRES_MAX) {
302        e->value.data = (u_char *) "Thu, 31 Dec 2037 23:55:55 GMT";
303        /* 10 years */
304        ngx_str_set(&cc->value, "max-age=315360000");
305        return NGX_OK;
306    }
307
308    e->value.data = ngx_pnalloc(r->pool, len);
309    if (e->value.data == NULL) {
310        return NGX_ERROR;
311    }
312
313    if (expires_time == 0 && expires != NGX_HTTP_EXPIRES_DAILY) {
314        ngx_memcpy(e->value.data, ngx_cached_http_time.data,
315                   ngx_cached_http_time.len + 1);
316        ngx_str_set(&cc->value, "max-age=0");
317        return NGX_OK;
318    }
319
320    now = ngx_time();
321
322    if (expires == NGX_HTTP_EXPIRES_DAILY) {
323        expires_time = ngx_next_time(expires_time);
324        max_age = expires_time - now;
325
326    } else if (expires == NGX_HTTP_EXPIRES_ACCESS
327               || r->headers_out.last_modified_time == -1)
328    {
329        max_age = expires_time;
330        expires_time += now;
331
332    } else {
333        expires_time += r->headers_out.last_modified_time;
334        max_age = expires_time - now;
335    }
336
337    ngx_http_time(e->value.data, expires_time);
338
339    if (conf->expires_time < 0 || max_age < 0) {
340        ngx_str_set(&cc->value, "no-cache");
341        return NGX_OK;
342    }
343
344    cc->value.data = ngx_pnalloc(r->pool,
345                                 sizeof("max-age=") + NGX_TIME_T_LEN + 1);
346    if (cc->value.data == NULL) {
347        return NGX_ERROR;
348    }
349
350    cc->value.len = ngx_sprintf(cc->value.data, "max-age=%T", max_age)
351                    - cc->value.data;
352
353    return NGX_OK;
354}
355
356
357static ngx_int_t
358ngx_http_parse_expires(ngx_str_t *value, ngx_http_expires_t *expires,
359    time_t *expires_time, char **err)
360{
361    ngx_uint_t  minus;
362
363    if (*expires != NGX_HTTP_EXPIRES_MODIFIED) {
364
365        if (value->len == 5 && ngx_strncmp(value->data, "epoch", 5) == 0) {
366            *expires = NGX_HTTP_EXPIRES_EPOCH;
367            return NGX_OK;
368        }
369
370        if (value->len == 3 && ngx_strncmp(value->data, "max", 3) == 0) {
371            *expires = NGX_HTTP_EXPIRES_MAX;
372            return NGX_OK;
373        }
374
375        if (value->len == 3 && ngx_strncmp(value->data, "off", 3) == 0) {
376            *expires = NGX_HTTP_EXPIRES_OFF;
377            return NGX_OK;
378        }
379    }
380
381    if (value->len && value->data[0] == '@') {
382        value->data++;
383        value->len--;
384        minus = 0;
385
386        if (*expires == NGX_HTTP_EXPIRES_MODIFIED) {
387            *err = "daily time cannot be used with \"modified\" parameter";
388            return NGX_ERROR;
389        }
390
391        *expires = NGX_HTTP_EXPIRES_DAILY;
392
393    } else if (value->len && value->data[0] == '+') {
394        value->data++;
395        value->len--;
396        minus = 0;
397
398    } else if (value->len && value->data[0] == '-') {
399        value->data++;
400        value->len--;
401        minus = 1;
402
403    } else {
404        minus = 0;
405    }
406
407    *expires_time = ngx_parse_time(value, 1);
408
409    if (*expires_time == (time_t) NGX_ERROR) {
410        *err = "invalid value";
411        return NGX_ERROR;
412    }
413
414    if (*expires == NGX_HTTP_EXPIRES_DAILY
415        && *expires_time > 24 * 60 * 60)
416    {
417        *err = "daily time value must be less than 24 hours";
418        return NGX_ERROR;
419    }
420
421    if (minus) {
422        *expires_time = - *expires_time;
423    }
424
425    return NGX_OK;
426}
427
428
429static ngx_int_t
430ngx_http_add_header(ngx_http_request_t *r, ngx_http_header_val_t *hv,
431    ngx_str_t *value)
432{
433    ngx_table_elt_t  *h;
434
435    if (value->len) {
436        h = ngx_list_push(&r->headers_out.headers);
437        if (h == NULL) {
438            return NGX_ERROR;
439        }
440
441        h->hash = 1;
442        h->key = hv->key;
443        h->value = *value;
444    }
445
446    return NGX_OK;
447}
448
449
450static ngx_int_t
451ngx_http_add_cache_control(ngx_http_request_t *r, ngx_http_header_val_t *hv,
452    ngx_str_t *value)
453{
454    ngx_table_elt_t  *cc, **ccp;
455
456    if (value->len == 0) {
457        return NGX_OK;
458    }
459
460    ccp = r->headers_out.cache_control.elts;
461
462    if (ccp == NULL) {
463
464        if (ngx_array_init(&r->headers_out.cache_control, r->pool,
465                           1, sizeof(ngx_table_elt_t *))
466            != NGX_OK)
467        {
468            return NGX_ERROR;
469        }
470    }
471
472    ccp = ngx_array_push(&r->headers_out.cache_control);
473    if (ccp == NULL) {
474        return NGX_ERROR;
475    }
476
477    cc = ngx_list_push(&r->headers_out.headers);
478    if (cc == NULL) {
479        return NGX_ERROR;
480    }
481
482    cc->hash = 1;
483    ngx_str_set(&cc->key, "Cache-Control");
484    cc->value = *value;
485
486    *ccp = cc;
487
488    return NGX_OK;
489}
490
491
492static ngx_int_t
493ngx_http_set_last_modified(ngx_http_request_t *r, ngx_http_header_val_t *hv,
494    ngx_str_t *value)
495{
496    if (ngx_http_set_response_header(r, hv, value) != NGX_OK) {
497        return NGX_ERROR;
498    }
499
500    r->headers_out.last_modified_time =
501        (value->len) ? ngx_parse_http_time(value->data, value->len) : -1;
502
503    return NGX_OK;
504}
505
506
507static ngx_int_t
508ngx_http_set_response_header(ngx_http_request_t *r, ngx_http_header_val_t *hv,
509    ngx_str_t *value)
510{
511    ngx_table_elt_t  *h, **old;
512
513    old = (ngx_table_elt_t **) ((char *) &r->headers_out + hv->offset);
514
515    if (value->len == 0) {
516        if (*old) {
517            (*old)->hash = 0;
518            *old = NULL;
519        }
520
521        return NGX_OK;
522    }
523
524    if (*old) {
525        h = *old;
526
527    } else {
528        h = ngx_list_push(&r->headers_out.headers);
529        if (h == NULL) {
530            return NGX_ERROR;
531        }
532
533        *old = h;
534    }
535
536    h->hash = 1;
537    h->key = hv->key;
538    h->value = *value;
539
540    return NGX_OK;
541}
542
543
544static void *
545ngx_http_headers_create_conf(ngx_conf_t *cf)
546{
547    ngx_http_headers_conf_t  *conf;
548
549    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_headers_conf_t));
550    if (conf == NULL) {
551        return NULL;
552    }
553
554    /*
555     * set by ngx_pcalloc():
556     *
557     *     conf->headers = NULL;
558     *     conf->expires_time = 0;
559     *     conf->expires_value = NULL;
560     */
561
562    conf->expires = NGX_HTTP_EXPIRES_UNSET;
563
564    return conf;
565}
566
567
568static char *
569ngx_http_headers_merge_conf(ngx_conf_t *cf, void *parent, void *child)
570{
571    ngx_http_headers_conf_t *prev = parent;
572    ngx_http_headers_conf_t *conf = child;
573
574    if (conf->expires == NGX_HTTP_EXPIRES_UNSET) {
575        conf->expires = prev->expires;
576        conf->expires_time = prev->expires_time;
577        conf->expires_value = prev->expires_value;
578
579        if (conf->expires == NGX_HTTP_EXPIRES_UNSET) {
580            conf->expires = NGX_HTTP_EXPIRES_OFF;
581        }
582    }
583
584    if (conf->headers == NULL) {
585        conf->headers = prev->headers;
586    }
587
588    return NGX_CONF_OK;
589}
590
591
592static ngx_int_t
593ngx_http_headers_filter_init(ngx_conf_t *cf)
594{
595    ngx_http_next_header_filter = ngx_http_top_header_filter;
596    ngx_http_top_header_filter = ngx_http_headers_filter;
597
598    return NGX_OK;
599}
600
601
602static char *
603ngx_http_headers_expires(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
604{
605    ngx_http_headers_conf_t *hcf = conf;
606
607    char                              *err;
608    ngx_str_t                         *value;
609    ngx_int_t                          rc;
610    ngx_uint_t                         n;
611    ngx_http_complex_value_t           cv;
612    ngx_http_compile_complex_value_t   ccv;
613
614    if (hcf->expires != NGX_HTTP_EXPIRES_UNSET) {
615        return "is duplicate";
616    }
617
618    value = cf->args->elts;
619
620    if (cf->args->nelts == 2) {
621
622        hcf->expires = NGX_HTTP_EXPIRES_ACCESS;
623
624        n = 1;
625
626    } else { /* cf->args->nelts == 3 */
627
628        if (ngx_strcmp(value[1].data, "modified") != 0) {
629            return "invalid value";
630        }
631
632        hcf->expires = NGX_HTTP_EXPIRES_MODIFIED;
633
634        n = 2;
635    }
636
637    ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t));
638
639    ccv.cf = cf;
640    ccv.value = &value[n];
641    ccv.complex_value = &cv;
642
643    if (ngx_http_compile_complex_value(&ccv) != NGX_OK) {
644        return NGX_CONF_ERROR;
645    }
646
647    if (cv.lengths != NULL) {
648
649        hcf->expires_value = ngx_palloc(cf->pool,
650                                        sizeof(ngx_http_complex_value_t));
651        if (hcf->expires_value == NULL) {
652            return NGX_CONF_ERROR;
653        }
654
655        *hcf->expires_value = cv;
656
657        return NGX_CONF_OK;
658    }
659
660    rc = ngx_http_parse_expires(&value[n], &hcf->expires, &hcf->expires_time,
661                                &err);
662    if (rc != NGX_OK) {
663        return err;
664    }
665
666    return NGX_CONF_OK;
667}
668
669
670static char *
671ngx_http_headers_add(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
672{
673    ngx_http_headers_conf_t *hcf = conf;
674
675    ngx_str_t                         *value;
676    ngx_uint_t                         i;
677    ngx_http_header_val_t             *hv;
678    ngx_http_set_header_t             *set;
679    ngx_http_compile_complex_value_t   ccv;
680
681    value = cf->args->elts;
682
683    if (hcf->headers == NULL) {
684        hcf->headers = ngx_array_create(cf->pool, 1,
685                                        sizeof(ngx_http_header_val_t));
686        if (hcf->headers == NULL) {
687            return NGX_CONF_ERROR;
688        }
689    }
690
691    hv = ngx_array_push(hcf->headers);
692    if (hv == NULL) {
693        return NGX_CONF_ERROR;
694    }
695
696    hv->key = value[1];
697    hv->handler = ngx_http_add_header;
698    hv->offset = 0;
699    hv->always = 0;
700
701    set = ngx_http_set_headers;
702    for (i = 0; set[i].name.len; i++) {
703        if (ngx_strcasecmp(value[1].data, set[i].name.data) != 0) {
704            continue;
705        }
706
707        hv->offset = set[i].offset;
708        hv->handler = set[i].handler;
709
710        break;
711    }
712
713    if (value[2].len == 0) {
714        ngx_memzero(&hv->value, sizeof(ngx_http_complex_value_t));
715
716    } else {
717        ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t));
718
719        ccv.cf = cf;
720        ccv.value = &value[2];
721        ccv.complex_value = &hv->value;
722
723        if (ngx_http_compile_complex_value(&ccv) != NGX_OK) {
724            return NGX_CONF_ERROR;
725        }
726    }
727
728    if (cf->args->nelts == 3) {
729        return NGX_CONF_OK;
730    }
731
732    if (ngx_strcmp(value[3].data, "always") != 0) {
733        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
734                           "invalid parameter \"%V\"", &value[3]);
735        return NGX_CONF_ERROR;
736    }
737
738    hv->always = 1;
739
740    return NGX_CONF_OK;
741}
742