| |
| /* |
| * Copyright (C) Igor Sysoev |
| * Copyright (C) Nginx, Inc. |
| */ |
| |
| |
| #include <ngx_config.h> |
| #include <ngx_core.h> |
| #include <ngx_http.h> |
| |
| |
| /* |
| * The module can check browser versions conforming to the following formats: |
| * X, X.X, X.X.X, and X.X.X.X. The maximum values of each format may be |
| * 4000, 4000.99, 4000.99.99, and 4000.99.99.99. |
| */ |
| |
| |
| #define NGX_HTTP_MODERN_BROWSER 0 |
| #define NGX_HTTP_ANCIENT_BROWSER 1 |
| |
| |
| typedef struct { |
| u_char browser[12]; |
| size_t skip; |
| size_t add; |
| u_char name[12]; |
| } ngx_http_modern_browser_mask_t; |
| |
| |
| typedef struct { |
| ngx_uint_t version; |
| size_t skip; |
| size_t add; |
| u_char name[12]; |
| } ngx_http_modern_browser_t; |
| |
| |
| typedef struct { |
| ngx_str_t name; |
| ngx_http_get_variable_pt handler; |
| uintptr_t data; |
| } ngx_http_browser_variable_t; |
| |
| |
| typedef struct { |
| ngx_array_t *modern_browsers; |
| ngx_array_t *ancient_browsers; |
| ngx_http_variable_value_t *modern_browser_value; |
| ngx_http_variable_value_t *ancient_browser_value; |
| |
| unsigned modern_unlisted_browsers:1; |
| unsigned netscape4:1; |
| } ngx_http_browser_conf_t; |
| |
| |
| static ngx_int_t ngx_http_msie_variable(ngx_http_request_t *r, |
| ngx_http_variable_value_t *v, uintptr_t data); |
| static ngx_int_t ngx_http_browser_variable(ngx_http_request_t *r, |
| ngx_http_variable_value_t *v, uintptr_t data); |
| |
| static ngx_uint_t ngx_http_browser(ngx_http_request_t *r, |
| ngx_http_browser_conf_t *cf); |
| |
| static ngx_int_t ngx_http_browser_add_variable(ngx_conf_t *cf); |
| static void *ngx_http_browser_create_conf(ngx_conf_t *cf); |
| static char *ngx_http_browser_merge_conf(ngx_conf_t *cf, void *parent, |
| void *child); |
| static int ngx_libc_cdecl ngx_http_modern_browser_sort(const void *one, |
| const void *two); |
| static char *ngx_http_modern_browser(ngx_conf_t *cf, ngx_command_t *cmd, |
| void *conf); |
| static char *ngx_http_ancient_browser(ngx_conf_t *cf, ngx_command_t *cmd, |
| void *conf); |
| static char *ngx_http_modern_browser_value(ngx_conf_t *cf, ngx_command_t *cmd, |
| void *conf); |
| static char *ngx_http_ancient_browser_value(ngx_conf_t *cf, ngx_command_t *cmd, |
| void *conf); |
| |
| |
| static ngx_command_t ngx_http_browser_commands[] = { |
| |
| { ngx_string("modern_browser"), |
| NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE12, |
| ngx_http_modern_browser, |
| NGX_HTTP_LOC_CONF_OFFSET, |
| 0, |
| NULL }, |
| |
| { ngx_string("ancient_browser"), |
| NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE, |
| ngx_http_ancient_browser, |
| NGX_HTTP_LOC_CONF_OFFSET, |
| 0, |
| NULL }, |
| |
| { ngx_string("modern_browser_value"), |
| NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, |
| ngx_http_modern_browser_value, |
| NGX_HTTP_LOC_CONF_OFFSET, |
| 0, |
| NULL }, |
| |
| { ngx_string("ancient_browser_value"), |
| NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, |
| ngx_http_ancient_browser_value, |
| NGX_HTTP_LOC_CONF_OFFSET, |
| 0, |
| NULL }, |
| |
| ngx_null_command |
| }; |
| |
| |
| static ngx_http_module_t ngx_http_browser_module_ctx = { |
| ngx_http_browser_add_variable, /* preconfiguration */ |
| NULL, /* postconfiguration */ |
| |
| NULL, /* create main configuration */ |
| NULL, /* init main configuration */ |
| |
| NULL, /* create server configuration */ |
| NULL, /* merge server configuration */ |
| |
| ngx_http_browser_create_conf, /* create location configuration */ |
| ngx_http_browser_merge_conf /* merge location configuration */ |
| }; |
| |
| |
| ngx_module_t ngx_http_browser_module = { |
| NGX_MODULE_V1, |
| &ngx_http_browser_module_ctx, /* module context */ |
| ngx_http_browser_commands, /* module directives */ |
| NGX_HTTP_MODULE, /* module type */ |
| NULL, /* init master */ |
| NULL, /* init module */ |
| NULL, /* init process */ |
| NULL, /* init thread */ |
| NULL, /* exit thread */ |
| NULL, /* exit process */ |
| NULL, /* exit master */ |
| NGX_MODULE_V1_PADDING |
| }; |
| |
| |
| static ngx_http_modern_browser_mask_t ngx_http_modern_browser_masks[] = { |
| |
| /* Opera must be the first browser to check */ |
| |
| /* |
| * "Opera/7.50 (X11; FreeBSD i386; U) [en]" |
| * "Mozilla/5.0 (X11; FreeBSD i386; U) Opera 7.50 [en]" |
| * "Mozilla/4.0 (compatible; MSIE 6.0; X11; FreeBSD i386) Opera 7.50 [en]" |
| * "Opera/8.0 (Windows NT 5.1; U; ru)" |
| * "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; en) Opera 8.0" |
| * "Opera/9.01 (X11; FreeBSD 6 i386; U; en)" |
| */ |
| |
| { "opera", |
| 0, |
| sizeof("Opera ") - 1, |
| "Opera"}, |
| |
| /* "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)" */ |
| |
| { "msie", |
| sizeof("Mozilla/4.0 (compatible; ") - 1, |
| sizeof("MSIE ") - 1, |
| "MSIE "}, |
| |
| /* |
| * "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.0.0) Gecko/20020610" |
| * "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU; rv:1.5) Gecko/20031006" |
| * "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU; rv:1.6) Gecko/20040206 |
| * Firefox/0.8" |
| * "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU; rv:1.7.8) |
| * Gecko/20050511 Firefox/1.0.4" |
| * "Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.8.0.5) Gecko/20060729 |
| * Firefox/1.5.0.5" |
| */ |
| |
| { "gecko", |
| sizeof("Mozilla/5.0 (") - 1, |
| sizeof("rv:") - 1, |
| "rv:"}, |
| |
| /* |
| * "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ru-ru) AppleWebKit/125.2 |
| * (KHTML, like Gecko) Safari/125.7" |
| * "Mozilla/5.0 (SymbianOS/9.1; U; en-us) AppleWebKit/413 |
| * (KHTML, like Gecko) Safari/413" |
| * "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418 |
| * (KHTML, like Gecko) Safari/417.9.3" |
| * "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ru-ru) AppleWebKit/418.8 |
| * (KHTML, like Gecko) Safari/419.3" |
| */ |
| |
| { "safari", |
| sizeof("Mozilla/5.0 (") - 1, |
| sizeof("Safari/") - 1, |
| "Safari/"}, |
| |
| /* |
| * "Mozilla/5.0 (compatible; Konqueror/3.1; Linux)" |
| * "Mozilla/5.0 (compatible; Konqueror/3.4; Linux) KHTML/3.4.2 (like Gecko)" |
| * "Mozilla/5.0 (compatible; Konqueror/3.5; FreeBSD) KHTML/3.5.1 |
| * (like Gecko)" |
| */ |
| |
| { "konqueror", |
| sizeof("Mozilla/5.0 (compatible; ") - 1, |
| sizeof("Konqueror/") - 1, |
| "Konqueror/"}, |
| |
| { "", 0, 0, "" } |
| |
| }; |
| |
| |
| static ngx_http_browser_variable_t ngx_http_browsers[] = { |
| { ngx_string("msie"), ngx_http_msie_variable, 0 }, |
| { ngx_string("modern_browser"), ngx_http_browser_variable, |
| NGX_HTTP_MODERN_BROWSER }, |
| { ngx_string("ancient_browser"), ngx_http_browser_variable, |
| NGX_HTTP_ANCIENT_BROWSER }, |
| { ngx_null_string, NULL, 0 } |
| }; |
| |
| |
| static ngx_int_t |
| ngx_http_browser_variable(ngx_http_request_t *r, ngx_http_variable_value_t *v, |
| uintptr_t data) |
| { |
| ngx_uint_t rc; |
| ngx_http_browser_conf_t *cf; |
| |
| cf = ngx_http_get_module_loc_conf(r, ngx_http_browser_module); |
| |
| rc = ngx_http_browser(r, cf); |
| |
| if (data == NGX_HTTP_MODERN_BROWSER && rc == NGX_HTTP_MODERN_BROWSER) { |
| *v = *cf->modern_browser_value; |
| return NGX_OK; |
| } |
| |
| if (data == NGX_HTTP_ANCIENT_BROWSER && rc == NGX_HTTP_ANCIENT_BROWSER) { |
| *v = *cf->ancient_browser_value; |
| return NGX_OK; |
| } |
| |
| *v = ngx_http_variable_null_value; |
| return NGX_OK; |
| } |
| |
| |
| static ngx_uint_t |
| ngx_http_browser(ngx_http_request_t *r, ngx_http_browser_conf_t *cf) |
| { |
| size_t len; |
| u_char *name, *ua, *last, c; |
| ngx_str_t *ancient; |
| ngx_uint_t i, version, ver, scale; |
| ngx_http_modern_browser_t *modern; |
| |
| if (r->headers_in.user_agent == NULL) { |
| if (cf->modern_unlisted_browsers) { |
| return NGX_HTTP_MODERN_BROWSER; |
| } |
| |
| return NGX_HTTP_ANCIENT_BROWSER; |
| } |
| |
| ua = r->headers_in.user_agent->value.data; |
| len = r->headers_in.user_agent->value.len; |
| last = ua + len; |
| |
| if (cf->modern_browsers) { |
| modern = cf->modern_browsers->elts; |
| |
| for (i = 0; i < cf->modern_browsers->nelts; i++) { |
| name = ua + modern[i].skip; |
| |
| if (name >= last) { |
| continue; |
| } |
| |
| name = (u_char *) ngx_strstr(name, modern[i].name); |
| |
| if (name == NULL) { |
| continue; |
| } |
| |
| ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| "browser: \"%s\"", name); |
| |
| name += modern[i].add; |
| |
| if (name >= last) { |
| continue; |
| } |
| |
| ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| "version: \"%ui\" \"%s\"", modern[i].version, name); |
| |
| version = 0; |
| ver = 0; |
| scale = 1000000; |
| |
| while (name < last) { |
| |
| c = *name++; |
| |
| if (c >= '0' && c <= '9') { |
| ver = ver * 10 + (c - '0'); |
| continue; |
| } |
| |
| if (c == '.') { |
| version += ver * scale; |
| |
| ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| "version: \"%ui\" \"%ui\"", |
| modern[i].version, version); |
| |
| if (version > modern[i].version) { |
| return NGX_HTTP_MODERN_BROWSER; |
| } |
| |
| ver = 0; |
| scale /= 100; |
| continue; |
| } |
| |
| break; |
| } |
| |
| version += ver * scale; |
| |
| ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| "version: \"%ui\" \"%ui\"", |
| modern[i].version, version); |
| |
| if (version >= modern[i].version) { |
| return NGX_HTTP_MODERN_BROWSER; |
| } |
| |
| return NGX_HTTP_ANCIENT_BROWSER; |
| } |
| |
| if (!cf->modern_unlisted_browsers) { |
| return NGX_HTTP_ANCIENT_BROWSER; |
| } |
| } |
| |
| if (cf->netscape4) { |
| if (len > sizeof("Mozilla/4.72 ") - 1 |
| && ngx_strncmp(ua, "Mozilla/", sizeof("Mozilla/") - 1) == 0 |
| && ua[8] > '0' && ua[8] < '5') |
| { |
| return NGX_HTTP_ANCIENT_BROWSER; |
| } |
| } |
| |
| if (cf->ancient_browsers) { |
| ancient = cf->ancient_browsers->elts; |
| |
| for (i = 0; i < cf->ancient_browsers->nelts; i++) { |
| if (len >= ancient[i].len |
| && ngx_strstr(ua, ancient[i].data) != NULL) |
| { |
| return NGX_HTTP_ANCIENT_BROWSER; |
| } |
| } |
| } |
| |
| if (cf->modern_unlisted_browsers) { |
| return NGX_HTTP_MODERN_BROWSER; |
| } |
| |
| return NGX_HTTP_ANCIENT_BROWSER; |
| } |
| |
| |
| static ngx_int_t |
| ngx_http_msie_variable(ngx_http_request_t *r, ngx_http_variable_value_t *v, |
| uintptr_t data) |
| { |
| if (r->headers_in.msie) { |
| *v = ngx_http_variable_true_value; |
| return NGX_OK; |
| } |
| |
| *v = ngx_http_variable_null_value; |
| return NGX_OK; |
| } |
| |
| |
| static ngx_int_t |
| ngx_http_browser_add_variable(ngx_conf_t *cf) |
| { |
| ngx_http_browser_variable_t *var; |
| ngx_http_variable_t *v; |
| |
| for (var = ngx_http_browsers; var->name.len; var++) { |
| |
| v = ngx_http_add_variable(cf, &var->name, NGX_HTTP_VAR_CHANGEABLE); |
| if (v == NULL) { |
| return NGX_ERROR; |
| } |
| |
| v->get_handler = var->handler; |
| v->data = var->data; |
| } |
| |
| return NGX_OK; |
| } |
| |
| |
| static void * |
| ngx_http_browser_create_conf(ngx_conf_t *cf) |
| { |
| ngx_http_browser_conf_t *conf; |
| |
| conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_browser_conf_t)); |
| if (conf == NULL) { |
| return NULL; |
| } |
| |
| /* |
| * set by ngx_pcalloc(): |
| * |
| * conf->modern_browsers = NULL; |
| * conf->ancient_browsers = NULL; |
| * conf->modern_browser_value = NULL; |
| * conf->ancient_browser_value = NULL; |
| * |
| * conf->modern_unlisted_browsers = 0; |
| * conf->netscape4 = 0; |
| */ |
| |
| return conf; |
| } |
| |
| |
| static char * |
| ngx_http_browser_merge_conf(ngx_conf_t *cf, void *parent, void *child) |
| { |
| ngx_http_browser_conf_t *prev = parent; |
| ngx_http_browser_conf_t *conf = child; |
| |
| ngx_uint_t i, n; |
| ngx_http_modern_browser_t *browsers, *opera; |
| |
| /* |
| * At the merge the skip field is used to store the browser slot, |
| * it will be used in sorting and then will overwritten |
| * with a real skip value. The zero value means Opera. |
| */ |
| |
| if (conf->modern_browsers == NULL && conf->modern_unlisted_browsers == 0) { |
| conf->modern_browsers = prev->modern_browsers; |
| conf->modern_unlisted_browsers = prev->modern_unlisted_browsers; |
| |
| } else if (conf->modern_browsers != NULL) { |
| browsers = conf->modern_browsers->elts; |
| |
| for (i = 0; i < conf->modern_browsers->nelts; i++) { |
| if (browsers[i].skip == 0) { |
| goto found; |
| } |
| } |
| |
| /* |
| * Opera may contain MSIE string, so if Opera was not enumerated |
| * as modern browsers, then add it and set a unreachable version |
| */ |
| |
| opera = ngx_array_push(conf->modern_browsers); |
| if (opera == NULL) { |
| return NGX_CONF_ERROR; |
| } |
| |
| opera->skip = 0; |
| opera->version = 4001000000U; |
| |
| browsers = conf->modern_browsers->elts; |
| |
| found: |
| |
| ngx_qsort(browsers, (size_t) conf->modern_browsers->nelts, |
| sizeof(ngx_http_modern_browser_t), |
| ngx_http_modern_browser_sort); |
| |
| for (i = 0; i < conf->modern_browsers->nelts; i++) { |
| n = browsers[i].skip; |
| |
| browsers[i].skip = ngx_http_modern_browser_masks[n].skip; |
| browsers[i].add = ngx_http_modern_browser_masks[n].add; |
| (void) ngx_cpystrn(browsers[i].name, |
| ngx_http_modern_browser_masks[n].name, 12); |
| } |
| } |
| |
| if (conf->ancient_browsers == NULL && conf->netscape4 == 0) { |
| conf->ancient_browsers = prev->ancient_browsers; |
| conf->netscape4 = prev->netscape4; |
| } |
| |
| if (conf->modern_browser_value == NULL) { |
| conf->modern_browser_value = prev->modern_browser_value; |
| } |
| |
| if (conf->modern_browser_value == NULL) { |
| conf->modern_browser_value = &ngx_http_variable_true_value; |
| } |
| |
| if (conf->ancient_browser_value == NULL) { |
| conf->ancient_browser_value = prev->ancient_browser_value; |
| } |
| |
| if (conf->ancient_browser_value == NULL) { |
| conf->ancient_browser_value = &ngx_http_variable_true_value; |
| } |
| |
| return NGX_CONF_OK; |
| } |
| |
| |
| static int ngx_libc_cdecl |
| ngx_http_modern_browser_sort(const void *one, const void *two) |
| { |
| ngx_http_modern_browser_t *first = (ngx_http_modern_browser_t *) one; |
| ngx_http_modern_browser_t *second = (ngx_http_modern_browser_t *) two; |
| |
| return (first->skip - second->skip); |
| } |
| |
| |
| static char * |
| ngx_http_modern_browser(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) |
| { |
| ngx_http_browser_conf_t *bcf = conf; |
| |
| u_char c; |
| ngx_str_t *value; |
| ngx_uint_t i, n, version, ver, scale; |
| ngx_http_modern_browser_t *browser; |
| ngx_http_modern_browser_mask_t *mask; |
| |
| value = cf->args->elts; |
| |
| if (cf->args->nelts == 2) { |
| if (ngx_strcmp(value[1].data, "unlisted") == 0) { |
| bcf->modern_unlisted_browsers = 1; |
| return NGX_CONF_OK; |
| } |
| |
| return NGX_CONF_ERROR; |
| } |
| |
| if (bcf->modern_browsers == NULL) { |
| bcf->modern_browsers = ngx_array_create(cf->pool, 5, |
| sizeof(ngx_http_modern_browser_t)); |
| if (bcf->modern_browsers == NULL) { |
| return NGX_CONF_ERROR; |
| } |
| } |
| |
| browser = ngx_array_push(bcf->modern_browsers); |
| if (browser == NULL) { |
| return NGX_CONF_ERROR; |
| } |
| |
| mask = ngx_http_modern_browser_masks; |
| |
| for (n = 0; mask[n].browser[0] != '\0'; n++) { |
| if (ngx_strcasecmp(mask[n].browser, value[1].data) == 0) { |
| goto found; |
| } |
| } |
| |
| ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, |
| "unknown browser name \"%V\"", &value[1]); |
| |
| return NGX_CONF_ERROR; |
| |
| found: |
| |
| /* |
| * at this stage the skip field is used to store the browser slot, |
| * it will be used in sorting in merge stage and then will overwritten |
| * with a real value |
| */ |
| |
| browser->skip = n; |
| |
| version = 0; |
| ver = 0; |
| scale = 1000000; |
| |
| for (i = 0; i < value[2].len; i++) { |
| |
| c = value[2].data[i]; |
| |
| if (c >= '0' && c <= '9') { |
| ver = ver * 10 + (c - '0'); |
| continue; |
| } |
| |
| if (c == '.') { |
| version += ver * scale; |
| ver = 0; |
| scale /= 100; |
| continue; |
| } |
| |
| ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, |
| "invalid browser version \"%V\"", &value[2]); |
| |
| return NGX_CONF_ERROR; |
| } |
| |
| version += ver * scale; |
| |
| browser->version = version; |
| |
| return NGX_CONF_OK; |
| } |
| |
| |
| static char * |
| ngx_http_ancient_browser(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) |
| { |
| ngx_http_browser_conf_t *bcf = conf; |
| |
| ngx_str_t *value, *browser; |
| ngx_uint_t i; |
| |
| value = cf->args->elts; |
| |
| for (i = 1; i < cf->args->nelts; i++) { |
| if (ngx_strcmp(value[i].data, "netscape4") == 0) { |
| bcf->netscape4 = 1; |
| continue; |
| } |
| |
| if (bcf->ancient_browsers == NULL) { |
| bcf->ancient_browsers = ngx_array_create(cf->pool, 4, |
| sizeof(ngx_str_t)); |
| if (bcf->ancient_browsers == NULL) { |
| return NGX_CONF_ERROR; |
| } |
| } |
| |
| browser = ngx_array_push(bcf->ancient_browsers); |
| if (browser == NULL) { |
| return NGX_CONF_ERROR; |
| } |
| |
| *browser = value[i]; |
| } |
| |
| return NGX_CONF_OK; |
| } |
| |
| |
| static char * |
| ngx_http_modern_browser_value(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) |
| { |
| ngx_http_browser_conf_t *bcf = conf; |
| |
| ngx_str_t *value; |
| |
| bcf->modern_browser_value = ngx_palloc(cf->pool, |
| sizeof(ngx_http_variable_value_t)); |
| if (bcf->modern_browser_value == NULL) { |
| return NGX_CONF_ERROR; |
| } |
| |
| value = cf->args->elts; |
| |
| bcf->modern_browser_value->len = value[1].len; |
| bcf->modern_browser_value->valid = 1; |
| bcf->modern_browser_value->no_cacheable = 0; |
| bcf->modern_browser_value->not_found = 0; |
| bcf->modern_browser_value->data = value[1].data; |
| |
| return NGX_CONF_OK; |
| } |
| |
| |
| static char * |
| ngx_http_ancient_browser_value(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) |
| { |
| ngx_http_browser_conf_t *bcf = conf; |
| |
| ngx_str_t *value; |
| |
| bcf->ancient_browser_value = ngx_palloc(cf->pool, |
| sizeof(ngx_http_variable_value_t)); |
| if (bcf->ancient_browser_value == NULL) { |
| return NGX_CONF_ERROR; |
| } |
| |
| value = cf->args->elts; |
| |
| bcf->ancient_browser_value->len = value[1].len; |
| bcf->ancient_browser_value->valid = 1; |
| bcf->ancient_browser_value->no_cacheable = 0; |
| bcf->ancient_browser_value->not_found = 0; |
| bcf->ancient_browser_value->data = value[1].data; |
| |
| return NGX_CONF_OK; |
| } |