/* * Copyright (C) Igor Sysoev */ #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. */ 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; } ngx_http_browser_t; typedef struct { ngx_array_t *modern_browsers; ngx_http_variable_value_t *value; } 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_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_modern_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_TAKE2, ngx_http_modern_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_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_t ngx_http_browsers[] = { { ngx_string("msie"), ngx_http_msie_variable }, { ngx_null_string, NULL } }; static ngx_int_t ngx_http_browser_variable(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) { u_char *name, *ua, *last, c; ngx_uint_t i, version, ver, scale; ngx_http_browser_conf_t *cf; ngx_http_modern_browser_t *browsers; cf = ngx_http_get_module_loc_conf(r, ngx_http_browser_module); if (cf->modern_browsers == NULL || cf->value == NULL) { *v = ngx_http_variable_null_value; return NGX_OK; } #if 0 if (!r->headers_in.msie && !r->headers_in.opera && !r->headers_in.gecko && !r->headers_in.konqueror) { *v = ngx_http_variable_null_value; return NGX_OK; } #endif if (r->headers_in.user_agent == NULL) { *v = ngx_http_variable_null_value; return NGX_OK; } ua = r->headers_in.user_agent->value.data; last = ua + r->headers_in.user_agent->value.len; browsers = cf->modern_browsers->elts; for (i = 0; i < cf->modern_browsers->nelts; i++) { name = ua + browsers[i].skip; if (name >= last) { continue; } name = (u_char *) ngx_strstr(name, browsers[i].name); if (name == NULL) { continue; } ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "browser: \"%s\"", name); name += browsers[i].add; if (name >= last) { continue; } ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "version: \"%ui\" \"%s\"", browsers[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; if (version > browsers[i].version) { *v = *cf->value; return NGX_OK; } ver = 0; scale /= 100; continue; } break; } version += ver * scale; ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "version: \"%ui\" \"%ui\"", browsers[i].version, version); if (version >= browsers[i].version) { *v = *cf->value; return NGX_OK; } } *v = ngx_http_variable_null_value; return NGX_OK; } 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_t *browser; ngx_http_variable_t *var; for (browser = ngx_http_browsers; browser->name.len; browser++) { var = ngx_http_add_variable(cf, &browser->name, NGX_HTTP_VAR_CHANGABLE); if (var == NULL) { return NGX_ERROR; } var->get_handler = browser->handler; } 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 NGX_CONF_ERROR; } /* * set by ngx_pcalloc(): * * conf->browsers = NULL; * conf->value = NULL; */ 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_browsers = prev->modern_browsers; } else { 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->value == NULL) { conf->value = prev->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; if (bcf->modern_browsers == NULL) { bcf->modern_browsers = ngx_array_create(cf->pool, 4, 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; } value = cf->args->elts; 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_modern_browser_value(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_browser_conf_t *bcf = conf; ngx_str_t *value, name; ngx_http_variable_t *var; value = cf->args->elts; name.len = sizeof("browser") - 1; name.data = (u_char *) "browser"; var = ngx_http_add_variable(cf, &name, NGX_HTTP_VAR_CHANGABLE); if (var == NULL) { return NGX_CONF_ERROR; } var->get_handler = ngx_http_browser_variable; bcf->value = ngx_palloc(cf->pool, sizeof(ngx_http_variable_value_t)); if (bcf->value == NULL) { return NGX_CONF_ERROR; } bcf->value->len = value[1].len; bcf->value->valid = 1; bcf->value->no_cachable = 0; bcf->value->not_found = 0; bcf->value->data = value[1].data; return NGX_CONF_OK; }