/* * Copyright (C) Igor Sysoev * Copyright (C) Nginx, Inc. */ #include #include #include /* * 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; }