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
13/*
14 * The module can check browser versions conforming to the following formats:
15 * X, X.X, X.X.X, and X.X.X.X.  The maximum values of each format may be
16 * 4000, 4000.99, 4000.99.99, and 4000.99.99.99.
17 */
18
19
20#define  NGX_HTTP_MODERN_BROWSER   0
21#define  NGX_HTTP_ANCIENT_BROWSER  1
22
23
24typedef struct {
25    u_char                      browser[12];
26    size_t                      skip;
27    size_t                      add;
28    u_char                      name[12];
29} ngx_http_modern_browser_mask_t;
30
31
32typedef struct {
33    ngx_uint_t                  version;
34    size_t                      skip;
35    size_t                      add;
36    u_char                      name[12];
37} ngx_http_modern_browser_t;
38
39
40typedef struct {
41    ngx_str_t                   name;
42    ngx_http_get_variable_pt    handler;
43    uintptr_t                   data;
44} ngx_http_browser_variable_t;
45
46
47typedef struct {
48    ngx_array_t                *modern_browsers;
49    ngx_array_t                *ancient_browsers;
50    ngx_http_variable_value_t  *modern_browser_value;
51    ngx_http_variable_value_t  *ancient_browser_value;
52
53    unsigned                    modern_unlisted_browsers:1;
54    unsigned                    netscape4:1;
55} ngx_http_browser_conf_t;
56
57
58static ngx_int_t ngx_http_msie_variable(ngx_http_request_t *r,
59    ngx_http_variable_value_t *v, uintptr_t data);
60static ngx_int_t ngx_http_browser_variable(ngx_http_request_t *r,
61    ngx_http_variable_value_t *v, uintptr_t data);
62
63static ngx_uint_t ngx_http_browser(ngx_http_request_t *r,
64    ngx_http_browser_conf_t *cf);
65
66static ngx_int_t ngx_http_browser_add_variable(ngx_conf_t *cf);
67static void *ngx_http_browser_create_conf(ngx_conf_t *cf);
68static char *ngx_http_browser_merge_conf(ngx_conf_t *cf, void *parent,
69    void *child);
70static int ngx_libc_cdecl ngx_http_modern_browser_sort(const void *one,
71    const void *two);
72static char *ngx_http_modern_browser(ngx_conf_t *cf, ngx_command_t *cmd,
73    void *conf);
74static char *ngx_http_ancient_browser(ngx_conf_t *cf, ngx_command_t *cmd,
75    void *conf);
76static char *ngx_http_modern_browser_value(ngx_conf_t *cf, ngx_command_t *cmd,
77    void *conf);
78static char *ngx_http_ancient_browser_value(ngx_conf_t *cf, ngx_command_t *cmd,
79    void *conf);
80
81
82static ngx_command_t  ngx_http_browser_commands[] = {
83
84    { ngx_string("modern_browser"),
85      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE12,
86      ngx_http_modern_browser,
87      NGX_HTTP_LOC_CONF_OFFSET,
88      0,
89      NULL },
90
91    { ngx_string("ancient_browser"),
92      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
93      ngx_http_ancient_browser,
94      NGX_HTTP_LOC_CONF_OFFSET,
95      0,
96      NULL },
97
98    { ngx_string("modern_browser_value"),
99      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
100      ngx_http_modern_browser_value,
101      NGX_HTTP_LOC_CONF_OFFSET,
102      0,
103      NULL },
104
105    { ngx_string("ancient_browser_value"),
106      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
107      ngx_http_ancient_browser_value,
108      NGX_HTTP_LOC_CONF_OFFSET,
109      0,
110      NULL },
111
112      ngx_null_command
113};
114
115
116static ngx_http_module_t  ngx_http_browser_module_ctx = {
117    ngx_http_browser_add_variable,         /* preconfiguration */
118    NULL,                                  /* postconfiguration */
119
120    NULL,                                  /* create main configuration */
121    NULL,                                  /* init main configuration */
122
123    NULL,                                  /* create server configuration */
124    NULL,                                  /* merge server configuration */
125
126    ngx_http_browser_create_conf,          /* create location configuration */
127    ngx_http_browser_merge_conf            /* merge location configuration */
128};
129
130
131ngx_module_t  ngx_http_browser_module = {
132    NGX_MODULE_V1,
133    &ngx_http_browser_module_ctx,          /* module context */
134    ngx_http_browser_commands,             /* module directives */
135    NGX_HTTP_MODULE,                       /* module type */
136    NULL,                                  /* init master */
137    NULL,                                  /* init module */
138    NULL,                                  /* init process */
139    NULL,                                  /* init thread */
140    NULL,                                  /* exit thread */
141    NULL,                                  /* exit process */
142    NULL,                                  /* exit master */
143    NGX_MODULE_V1_PADDING
144};
145
146
147static ngx_http_modern_browser_mask_t  ngx_http_modern_browser_masks[] = {
148
149    /* Opera must be the first browser to check */
150
151    /*
152     * "Opera/7.50 (X11; FreeBSD i386; U)  [en]"
153     * "Mozilla/5.0 (X11; FreeBSD i386; U) Opera 7.50  [en]"
154     * "Mozilla/4.0 (compatible; MSIE 6.0; X11; FreeBSD i386) Opera 7.50  [en]"
155     * "Opera/8.0 (Windows NT 5.1; U; ru)"
156     * "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; en) Opera 8.0"
157     * "Opera/9.01 (X11; FreeBSD 6 i386; U; en)"
158     */
159
160    { "opera",
161      0,
162      sizeof("Opera ") - 1,
163      "Opera"},
164
165    /* "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)" */
166
167    { "msie",
168      sizeof("Mozilla/4.0 (compatible; ") - 1,
169      sizeof("MSIE ") - 1,
170      "MSIE "},
171
172    /*
173     * "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.0.0) Gecko/20020610"
174     * "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU; rv:1.5) Gecko/20031006"
175     * "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU; rv:1.6) Gecko/20040206
176     *              Firefox/0.8"
177     * "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU; rv:1.7.8)
178     *              Gecko/20050511 Firefox/1.0.4"
179     * "Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.8.0.5) Gecko/20060729
180     *              Firefox/1.5.0.5"
181     */
182
183    { "gecko",
184      sizeof("Mozilla/5.0 (") - 1,
185      sizeof("rv:") - 1,
186      "rv:"},
187
188    /*
189     * "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ru-ru) AppleWebKit/125.2
190     *              (KHTML, like Gecko) Safari/125.7"
191     * "Mozilla/5.0 (SymbianOS/9.1; U; en-us) AppleWebKit/413
192     *              (KHTML, like Gecko) Safari/413"
193     * "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418
194     *              (KHTML, like Gecko) Safari/417.9.3"
195     * "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ru-ru) AppleWebKit/418.8
196     *              (KHTML, like Gecko) Safari/419.3"
197     */
198
199    { "safari",
200      sizeof("Mozilla/5.0 (") - 1,
201      sizeof("Safari/") - 1,
202      "Safari/"},
203
204    /*
205     * "Mozilla/5.0 (compatible; Konqueror/3.1; Linux)"
206     * "Mozilla/5.0 (compatible; Konqueror/3.4; Linux) KHTML/3.4.2 (like Gecko)"
207     * "Mozilla/5.0 (compatible; Konqueror/3.5; FreeBSD) KHTML/3.5.1
208     *              (like Gecko)"
209     */
210
211    { "konqueror",
212      sizeof("Mozilla/5.0 (compatible; ") - 1,
213      sizeof("Konqueror/") - 1,
214      "Konqueror/"},
215
216    { "", 0, 0, "" }
217
218};
219
220
221static ngx_http_browser_variable_t  ngx_http_browsers[] = {
222    { ngx_string("msie"), ngx_http_msie_variable, 0 },
223    { ngx_string("modern_browser"), ngx_http_browser_variable,
224          NGX_HTTP_MODERN_BROWSER },
225    { ngx_string("ancient_browser"), ngx_http_browser_variable,
226          NGX_HTTP_ANCIENT_BROWSER },
227    { ngx_null_string, NULL, 0 }
228};
229
230
231static ngx_int_t
232ngx_http_browser_variable(ngx_http_request_t *r, ngx_http_variable_value_t *v,
233    uintptr_t data)
234{
235    ngx_uint_t                rc;
236    ngx_http_browser_conf_t  *cf;
237
238    cf = ngx_http_get_module_loc_conf(r, ngx_http_browser_module);
239
240    rc = ngx_http_browser(r, cf);
241
242    if (data == NGX_HTTP_MODERN_BROWSER && rc == NGX_HTTP_MODERN_BROWSER) {
243        *v = *cf->modern_browser_value;
244        return NGX_OK;
245    }
246
247    if (data == NGX_HTTP_ANCIENT_BROWSER && rc == NGX_HTTP_ANCIENT_BROWSER) {
248        *v = *cf->ancient_browser_value;
249        return NGX_OK;
250    }
251
252    *v = ngx_http_variable_null_value;
253    return NGX_OK;
254}
255
256
257static ngx_uint_t
258ngx_http_browser(ngx_http_request_t *r, ngx_http_browser_conf_t *cf)
259{
260    size_t                      len;
261    u_char                     *name, *ua, *last, c;
262    ngx_str_t                  *ancient;
263    ngx_uint_t                  i, version, ver, scale;
264    ngx_http_modern_browser_t  *modern;
265
266    if (r->headers_in.user_agent == NULL) {
267        if (cf->modern_unlisted_browsers) {
268            return NGX_HTTP_MODERN_BROWSER;
269        }
270
271        return NGX_HTTP_ANCIENT_BROWSER;
272    }
273
274    ua = r->headers_in.user_agent->value.data;
275    len = r->headers_in.user_agent->value.len;
276    last = ua + len;
277
278    if (cf->modern_browsers) {
279        modern = cf->modern_browsers->elts;
280
281        for (i = 0; i < cf->modern_browsers->nelts; i++) {
282            name = ua + modern[i].skip;
283
284            if (name >= last) {
285                continue;
286            }
287
288            name = (u_char *) ngx_strstr(name, modern[i].name);
289
290            if (name == NULL) {
291                continue;
292            }
293
294            ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
295                           "browser: \"%s\"", name);
296
297            name += modern[i].add;
298
299            if (name >= last) {
300                continue;
301            }
302
303            ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
304                           "version: \"%ui\" \"%s\"", modern[i].version, name);
305
306            version = 0;
307            ver = 0;
308            scale = 1000000;
309
310            while (name < last) {
311
312                c = *name++;
313
314                if (c >= '0' && c <= '9') {
315                    ver = ver * 10 + (c - '0');
316                    continue;
317                }
318
319                if (c == '.') {
320                    version += ver * scale;
321
322                    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
323                                   "version: \"%ui\" \"%ui\"",
324                                   modern[i].version, version);
325
326                    if (version > modern[i].version) {
327                        return NGX_HTTP_MODERN_BROWSER;
328                    }
329
330                    ver = 0;
331                    scale /= 100;
332                    continue;
333                }
334
335                break;
336            }
337
338            version += ver * scale;
339
340            ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
341                           "version: \"%ui\" \"%ui\"",
342                           modern[i].version, version);
343
344            if (version >= modern[i].version) {
345                return NGX_HTTP_MODERN_BROWSER;
346            }
347
348            return NGX_HTTP_ANCIENT_BROWSER;
349        }
350
351        if (!cf->modern_unlisted_browsers) {
352            return NGX_HTTP_ANCIENT_BROWSER;
353        }
354    }
355
356    if (cf->netscape4) {
357        if (len > sizeof("Mozilla/4.72 ") - 1
358            && ngx_strncmp(ua, "Mozilla/", sizeof("Mozilla/") - 1) == 0
359            && ua[8] > '0' && ua[8] < '5')
360        {
361            return NGX_HTTP_ANCIENT_BROWSER;
362        }
363    }
364
365    if (cf->ancient_browsers) {
366        ancient = cf->ancient_browsers->elts;
367
368        for (i = 0; i < cf->ancient_browsers->nelts; i++) {
369            if (len >= ancient[i].len
370                && ngx_strstr(ua, ancient[i].data) != NULL)
371            {
372                return NGX_HTTP_ANCIENT_BROWSER;
373            }
374        }
375    }
376
377    if (cf->modern_unlisted_browsers) {
378        return NGX_HTTP_MODERN_BROWSER;
379    }
380
381    return NGX_HTTP_ANCIENT_BROWSER;
382}
383
384
385static ngx_int_t
386ngx_http_msie_variable(ngx_http_request_t *r, ngx_http_variable_value_t *v,
387    uintptr_t data)
388{
389    if (r->headers_in.msie) {
390        *v = ngx_http_variable_true_value;
391        return NGX_OK;
392    }
393
394    *v = ngx_http_variable_null_value;
395    return NGX_OK;
396}
397
398
399static ngx_int_t
400ngx_http_browser_add_variable(ngx_conf_t *cf)
401{
402    ngx_http_browser_variable_t   *var;
403    ngx_http_variable_t           *v;
404
405    for (var = ngx_http_browsers; var->name.len; var++) {
406
407        v = ngx_http_add_variable(cf, &var->name, NGX_HTTP_VAR_CHANGEABLE);
408        if (v == NULL) {
409            return NGX_ERROR;
410        }
411
412        v->get_handler = var->handler;
413        v->data = var->data;
414    }
415
416    return NGX_OK;
417}
418
419
420static void *
421ngx_http_browser_create_conf(ngx_conf_t *cf)
422{
423    ngx_http_browser_conf_t  *conf;
424
425    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_browser_conf_t));
426    if (conf == NULL) {
427        return NULL;
428    }
429
430    /*
431     * set by ngx_pcalloc():
432     *
433     *     conf->modern_browsers = NULL;
434     *     conf->ancient_browsers = NULL;
435     *     conf->modern_browser_value = NULL;
436     *     conf->ancient_browser_value = NULL;
437     *
438     *     conf->modern_unlisted_browsers = 0;
439     *     conf->netscape4 = 0;
440     */
441
442    return conf;
443}
444
445
446static char *
447ngx_http_browser_merge_conf(ngx_conf_t *cf, void *parent, void *child)
448{
449    ngx_http_browser_conf_t *prev = parent;
450    ngx_http_browser_conf_t *conf = child;
451
452    ngx_uint_t                  i, n;
453    ngx_http_modern_browser_t  *browsers, *opera;
454
455    /*
456     * At the merge the skip field is used to store the browser slot,
457     * it will be used in sorting and then will overwritten
458     * with a real skip value.  The zero value means Opera.
459     */
460
461    if (conf->modern_browsers == NULL && conf->modern_unlisted_browsers == 0) {
462        conf->modern_browsers = prev->modern_browsers;
463        conf->modern_unlisted_browsers = prev->modern_unlisted_browsers;
464
465    } else if (conf->modern_browsers != NULL) {
466        browsers = conf->modern_browsers->elts;
467
468        for (i = 0; i < conf->modern_browsers->nelts; i++) {
469            if (browsers[i].skip == 0) {
470                goto found;
471            }
472        }
473
474        /*
475         * Opera may contain MSIE string, so if Opera was not enumerated
476         * as modern browsers, then add it and set a unreachable version
477         */
478
479        opera = ngx_array_push(conf->modern_browsers);
480        if (opera == NULL) {
481            return NGX_CONF_ERROR;
482        }
483
484        opera->skip = 0;
485        opera->version = 4001000000U;
486
487        browsers = conf->modern_browsers->elts;
488
489found:
490
491        ngx_qsort(browsers, (size_t) conf->modern_browsers->nelts,
492                  sizeof(ngx_http_modern_browser_t),
493                  ngx_http_modern_browser_sort);
494
495        for (i = 0; i < conf->modern_browsers->nelts; i++) {
496             n = browsers[i].skip;
497
498             browsers[i].skip = ngx_http_modern_browser_masks[n].skip;
499             browsers[i].add = ngx_http_modern_browser_masks[n].add;
500             (void) ngx_cpystrn(browsers[i].name,
501                                ngx_http_modern_browser_masks[n].name, 12);
502        }
503    }
504
505    if (conf->ancient_browsers == NULL && conf->netscape4 == 0) {
506        conf->ancient_browsers = prev->ancient_browsers;
507        conf->netscape4 = prev->netscape4;
508    }
509
510    if (conf->modern_browser_value == NULL) {
511        conf->modern_browser_value = prev->modern_browser_value;
512    }
513
514    if (conf->modern_browser_value == NULL) {
515        conf->modern_browser_value = &ngx_http_variable_true_value;
516    }
517
518    if (conf->ancient_browser_value == NULL) {
519        conf->ancient_browser_value = prev->ancient_browser_value;
520    }
521
522    if (conf->ancient_browser_value == NULL) {
523        conf->ancient_browser_value = &ngx_http_variable_true_value;
524    }
525
526    return NGX_CONF_OK;
527}
528
529
530static int ngx_libc_cdecl
531ngx_http_modern_browser_sort(const void *one, const void *two)
532{
533    ngx_http_modern_browser_t *first = (ngx_http_modern_browser_t *) one;
534    ngx_http_modern_browser_t *second = (ngx_http_modern_browser_t *) two;
535
536    return (first->skip - second->skip);
537}
538
539
540static char *
541ngx_http_modern_browser(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
542{
543    ngx_http_browser_conf_t *bcf = conf;
544
545    u_char                           c;
546    ngx_str_t                       *value;
547    ngx_uint_t                       i, n, version, ver, scale;
548    ngx_http_modern_browser_t       *browser;
549    ngx_http_modern_browser_mask_t  *mask;
550
551    value = cf->args->elts;
552
553    if (cf->args->nelts == 2) {
554        if (ngx_strcmp(value[1].data, "unlisted") == 0) {
555            bcf->modern_unlisted_browsers = 1;
556            return NGX_CONF_OK;
557        }
558
559        return NGX_CONF_ERROR;
560    }
561
562    if (bcf->modern_browsers == NULL) {
563        bcf->modern_browsers = ngx_array_create(cf->pool, 5,
564                                            sizeof(ngx_http_modern_browser_t));
565        if (bcf->modern_browsers == NULL) {
566            return NGX_CONF_ERROR;
567        }
568    }
569
570    browser = ngx_array_push(bcf->modern_browsers);
571    if (browser == NULL) {
572        return NGX_CONF_ERROR;
573    }
574
575    mask = ngx_http_modern_browser_masks;
576
577    for (n = 0; mask[n].browser[0] != '\0'; n++) {
578        if (ngx_strcasecmp(mask[n].browser, value[1].data) == 0) {
579            goto found;
580        }
581    }
582
583    ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
584                       "unknown browser name \"%V\"", &value[1]);
585
586    return NGX_CONF_ERROR;
587
588found:
589
590    /*
591     * at this stage the skip field is used to store the browser slot,
592     * it will be used in sorting in merge stage and then will overwritten
593     * with a real value
594     */
595
596    browser->skip = n;
597
598    version = 0;
599    ver = 0;
600    scale = 1000000;
601
602    for (i = 0; i < value[2].len; i++) {
603
604        c = value[2].data[i];
605
606        if (c >= '0' && c <= '9') {
607            ver = ver * 10 + (c - '0');
608            continue;
609        }
610
611        if (c == '.') {
612            version += ver * scale;
613            ver = 0;
614            scale /= 100;
615            continue;
616        }
617
618        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
619                           "invalid browser version \"%V\"", &value[2]);
620
621        return NGX_CONF_ERROR;
622    }
623
624    version += ver * scale;
625
626    browser->version = version;
627
628    return NGX_CONF_OK;
629}
630
631
632static char *
633ngx_http_ancient_browser(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
634{
635    ngx_http_browser_conf_t *bcf = conf;
636
637    ngx_str_t   *value, *browser;
638    ngx_uint_t   i;
639
640    value = cf->args->elts;
641
642    for (i = 1; i < cf->args->nelts; i++) {
643        if (ngx_strcmp(value[i].data, "netscape4") == 0) {
644            bcf->netscape4 = 1;
645            continue;
646        }
647
648        if (bcf->ancient_browsers == NULL) {
649            bcf->ancient_browsers = ngx_array_create(cf->pool, 4,
650                                                     sizeof(ngx_str_t));
651            if (bcf->ancient_browsers == NULL) {
652                return NGX_CONF_ERROR;
653            }
654        }
655
656        browser = ngx_array_push(bcf->ancient_browsers);
657        if (browser == NULL) {
658            return NGX_CONF_ERROR;
659        }
660
661        *browser = value[i];
662    }
663
664    return NGX_CONF_OK;
665}
666
667
668static char *
669ngx_http_modern_browser_value(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
670{
671    ngx_http_browser_conf_t *bcf = conf;
672
673    ngx_str_t  *value;
674
675    bcf->modern_browser_value = ngx_palloc(cf->pool,
676                                           sizeof(ngx_http_variable_value_t));
677    if (bcf->modern_browser_value == NULL) {
678        return NGX_CONF_ERROR;
679    }
680
681    value = cf->args->elts;
682
683    bcf->modern_browser_value->len = value[1].len;
684    bcf->modern_browser_value->valid = 1;
685    bcf->modern_browser_value->no_cacheable = 0;
686    bcf->modern_browser_value->not_found = 0;
687    bcf->modern_browser_value->data = value[1].data;
688
689    return NGX_CONF_OK;
690}
691
692
693static char *
694ngx_http_ancient_browser_value(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
695{
696    ngx_http_browser_conf_t *bcf = conf;
697
698    ngx_str_t  *value;
699
700    bcf->ancient_browser_value = ngx_palloc(cf->pool,
701                                            sizeof(ngx_http_variable_value_t));
702    if (bcf->ancient_browser_value == NULL) {
703        return NGX_CONF_ERROR;
704    }
705
706    value = cf->args->elts;
707
708    bcf->ancient_browser_value->len = value[1].len;
709    bcf->ancient_browser_value->valid = 1;
710    bcf->ancient_browser_value->no_cacheable = 0;
711    bcf->ancient_browser_value->not_found = 0;
712    bcf->ancient_browser_value->data = value[1].data;
713
714    return NGX_CONF_OK;
715}
716