# HG changeset patch # User Igor Sysoev # Date 1158582307 0 # Node ID c1104f701926c1be74e94642f66ea98f9faab777 # Parent 1f3cc64393b695d8639364ef76d6b771ebc4bd5c ngx_http_browser_module diff --git a/auto/modules b/auto/modules --- a/auto/modules +++ b/auto/modules @@ -237,6 +237,11 @@ if [ $HTTP_EMPTY_GIF = YES ]; then HTTP_SRCS="$HTTP_SRCS $HTTP_EMPTY_GIF_SRCS" fi +if [ $HTTP_BROWSER = YES ]; then + HTTP_MODULES="$HTTP_MODULES $HTTP_BROWSER_MODULE" + HTTP_SRCS="$HTTP_SRCS $HTTP_BROWSER_SRCS" +fi + # STUB #USE_MD5=YES #HTTP_SRCS="$HTTP_SRCS $HTPP_CACHE_SRCS" diff --git a/auto/options b/auto/options --- a/auto/options +++ b/auto/options @@ -68,6 +68,7 @@ HTTP_FASTCGI=YES HTTP_PERL=NO HTTP_MEMCACHED=YES HTTP_EMPTY_GIF=YES +HTTP_BROWSER=YES # STUB HTTP_STUB_STATUS=NO @@ -165,6 +166,7 @@ do --without-http_fastcgi_module) HTTP_FASTCGI=NO ;; --without-http_memcached_module) HTTP_MEMCACHED=NO ;; --without-http_empty_gif_module) HTTP_EMPTY_GIF=NO ;; + --without-http_browser_module) HTTP_BROWSER=NO ;; --with-http_perl_module) HTTP_PERL=YES ;; --with-perl_modules_path=*) NGX_PERL_MODULES="$value" ;; diff --git a/auto/sources b/auto/sources --- a/auto/sources +++ b/auto/sources @@ -386,6 +386,10 @@ HTTP_EMPTY_GIF_MODULE=ngx_http_empty_gif HTTP_EMPTY_GIF_SRCS=src/http/modules/ngx_http_empty_gif_module.c +HTTP_BROWSER_MODULE=ngx_http_browser_module +HTTP_BROWSER_SRCS=src/http/modules/ngx_http_browser_module.c + + IMAP_INCS="src/imap" IMAP_DEPS="src/imap/ngx_imap.h" diff --git a/src/http/modules/ngx_http_browser_module.c b/src/http/modules/ngx_http_browser_module.c new file mode 100644 --- /dev/null +++ b/src/http/modules/ngx_http_browser_module.c @@ -0,0 +1,551 @@ + +/* + * 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) { + *v = ngx_http_variable_null_value; + } + + 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; + + ngx_conf_log_error(NGX_LOG_NOTICE, cf, 0, + "browser version \"%V\", %ui", &value[2], version); + + 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; +}