/* * Copyright (C) Igor Sysoev */ #include #include #include typedef struct { ngx_http_variable_value_t *value; u_short start; u_short end; } ngx_http_geo_range_t; typedef struct { ngx_http_geo_range_t *low[0x10000]; ngx_http_variable_value_t *default_value; } ngx_http_geo_high_ranges_t; typedef struct { ngx_str_node_t sn; ngx_http_variable_value_t *value; } ngx_http_geo_variable_value_node_t; typedef struct { ngx_http_variable_value_t *value; ngx_str_t *net; ngx_http_geo_high_ranges_t *high; ngx_radix_tree_t *tree; ngx_rbtree_t rbtree; ngx_rbtree_node_t sentinel; ngx_array_t *proxies; ngx_pool_t *pool; ngx_pool_t *temp_pool; } ngx_http_geo_conf_ctx_t; typedef struct { union { ngx_radix_tree_t *tree; ngx_http_geo_high_ranges_t *high; } u; ngx_array_t *proxies; ngx_int_t index; } ngx_http_geo_ctx_t; static in_addr_t ngx_http_geo_addr(ngx_http_request_t *r, ngx_http_geo_ctx_t *ctx); static in_addr_t ngx_http_geo_real_addr(ngx_http_request_t *r, ngx_http_geo_ctx_t *ctx); static char *ngx_http_geo_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static char *ngx_http_geo(ngx_conf_t *cf, ngx_command_t *dummy, void *conf); static char *ngx_http_geo_range(ngx_conf_t *cf, ngx_http_geo_conf_ctx_t *ctx, ngx_str_t *value); static char *ngx_http_geo_add_range(ngx_conf_t *cf, ngx_http_geo_conf_ctx_t *ctx, in_addr_t start, in_addr_t end); static ngx_uint_t ngx_http_geo_delete_range(ngx_conf_t *cf, ngx_http_geo_conf_ctx_t *ctx, in_addr_t start, in_addr_t end); static char *ngx_http_geo_cidr(ngx_conf_t *cf, ngx_http_geo_conf_ctx_t *ctx, ngx_str_t *value); static ngx_http_variable_value_t *ngx_http_geo_value(ngx_conf_t *cf, ngx_http_geo_conf_ctx_t *ctx, ngx_str_t *value); static char *ngx_http_geo_add_proxy(ngx_conf_t *cf, ngx_http_geo_conf_ctx_t *ctx, ngx_cidr_t *cidr); static ngx_int_t ngx_http_geo_cidr_value(ngx_conf_t *cf, ngx_str_t *net, ngx_cidr_t *cidr); static ngx_command_t ngx_http_geo_commands[] = { { ngx_string("geo"), NGX_HTTP_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_TAKE12, ngx_http_geo_block, NGX_HTTP_MAIN_CONF_OFFSET, 0, NULL }, ngx_null_command }; static ngx_http_module_t ngx_http_geo_module_ctx = { NULL, /* preconfiguration */ NULL, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ NULL, /* create location configuration */ NULL /* merge location configuration */ }; ngx_module_t ngx_http_geo_module = { NGX_MODULE_V1, &ngx_http_geo_module_ctx, /* module context */ ngx_http_geo_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 }; /* AF_INET only */ static ngx_int_t ngx_http_geo_cidr_variable(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) { ngx_http_geo_ctx_t *ctx = (ngx_http_geo_ctx_t *) data; ngx_http_variable_value_t *vv; vv = (ngx_http_variable_value_t *) ngx_radix32tree_find(ctx->u.tree, ngx_http_geo_addr(r, ctx)); *v = *vv; ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "http geo: %v", v); return NGX_OK; } static ngx_int_t ngx_http_geo_range_variable(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) { ngx_http_geo_ctx_t *ctx = (ngx_http_geo_ctx_t *) data; in_addr_t addr; ngx_uint_t n; ngx_http_geo_range_t *range; *v = *ctx->u.high->default_value; addr = ngx_http_geo_addr(r, ctx); range = ctx->u.high->low[addr >> 16]; if (range) { n = addr & 0xffff; do { if (n >= (ngx_uint_t) range->start && n <= (ngx_uint_t) range->end) { *v = *range->value; break; } } while ((++range)->value); } ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "http geo: %v", v); return NGX_OK; } static in_addr_t ngx_http_geo_addr(ngx_http_request_t *r, ngx_http_geo_ctx_t *ctx) { u_char *p, *ip; size_t len; in_addr_t addr; ngx_uint_t i, n; ngx_in_cidr_t *proxies; ngx_table_elt_t *xfwd; addr = ngx_http_geo_real_addr(r, ctx); xfwd = r->headers_in.x_forwarded_for; if (xfwd == NULL || ctx->proxies == NULL) { return addr; } proxies = ctx->proxies->elts; n = ctx->proxies->nelts; for (i = 0; i < n; i++) { if ((addr & proxies[i].mask) == proxies[i].addr) { len = xfwd->value.len; ip = xfwd->value.data; for (p = ip + len - 1; p > ip; p--) { if (*p == ' ' || *p == ',') { p++; len -= p - ip; ip = p; break; } } return ntohl(ngx_inet_addr(ip, len)); } } return addr; } static in_addr_t ngx_http_geo_real_addr(ngx_http_request_t *r, ngx_http_geo_ctx_t *ctx) { struct sockaddr_in *sin; ngx_http_variable_value_t *v; if (ctx->index == -1) { ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "http geo started: %V", &r->connection->addr_text); if (r->connection->sockaddr->sa_family != AF_INET) { return 0; } sin = (struct sockaddr_in *) r->connection->sockaddr; return ntohl(sin->sin_addr.s_addr); } v = ngx_http_get_flushed_variable(r, ctx->index); if (v == NULL || v->not_found) { ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "http geo not found"); return 0; } ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "http geo started: %v", v); return ntohl(ngx_inet_addr(v->data, v->len)); } static char * ngx_http_geo_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { char *rv; void **p; size_t len; ngx_str_t *value, name; ngx_uint_t i; ngx_conf_t save; ngx_pool_t *pool; ngx_array_t *a; ngx_http_variable_t *var; ngx_http_geo_ctx_t *geo; ngx_http_geo_conf_ctx_t ctx; value = cf->args->elts; geo = ngx_palloc(cf->pool, sizeof(ngx_http_geo_ctx_t)); if (geo == NULL) { return NGX_CONF_ERROR; } name = value[1]; name.len--; name.data++; if (cf->args->nelts == 3) { geo->index = ngx_http_get_variable_index(cf, &name); if (geo->index == NGX_ERROR) { return NGX_CONF_ERROR; } name = value[2]; name.len--; name.data++; } else { geo->index = -1; } var = ngx_http_add_variable(cf, &name, NGX_HTTP_VAR_CHANGEABLE); if (var == NULL) { return NGX_CONF_ERROR; } pool = ngx_create_pool(16384, cf->log); if (pool == NULL) { return NGX_CONF_ERROR; } ctx.temp_pool = ngx_create_pool(16384, cf->log); if (ctx.temp_pool == NULL) { return NGX_CONF_ERROR; } ngx_rbtree_init(&ctx.rbtree, &ctx.sentinel, ngx_str_rbtree_insert_value); ctx.high = NULL; ctx.tree = NULL; ctx.proxies = NULL; ctx.pool = cf->pool; save = *cf; cf->pool = pool; cf->ctx = &ctx; cf->handler = ngx_http_geo; cf->handler_conf = conf; rv = ngx_conf_parse(cf, NULL); *cf = save; geo->proxies = ctx.proxies; if (ctx.high) { for (i = 0; i < 0x10000; i++) { a = (ngx_array_t *) ctx.high->low[i]; if (a == NULL || a->nelts == 0) { continue; } len = a->nelts * sizeof(ngx_http_geo_range_t); ctx.high->low[i] = ngx_palloc(cf->pool, len + sizeof(void *)); if (ctx.high->low[i] == NULL) { return NGX_CONF_ERROR; } p = (void **) ngx_cpymem(ctx.high->low[i], a->elts, len); *p = NULL; } geo->u.high = ctx.high; var->get_handler = ngx_http_geo_range_variable; var->data = (uintptr_t) geo; ngx_destroy_pool(ctx.temp_pool); ngx_destroy_pool(pool); if (ctx.high->default_value == NULL) { ctx.high->default_value = &ngx_http_variable_null_value; } } else { if (ctx.tree == NULL) { ctx.tree = ngx_radix_tree_create(cf->pool, -1); if (ctx.tree == NULL) { return NGX_CONF_ERROR; } } geo->u.tree = ctx.tree; var->get_handler = ngx_http_geo_cidr_variable; var->data = (uintptr_t) geo; ngx_destroy_pool(ctx.temp_pool); ngx_destroy_pool(pool); if (ngx_radix32tree_find(ctx.tree, 0) != NGX_RADIX_NO_VALUE) { return rv; } if (ngx_radix32tree_insert(ctx.tree, 0, 0, (uintptr_t) &ngx_http_variable_null_value) == NGX_ERROR) { return NGX_CONF_ERROR; } } return rv; } static char * ngx_http_geo(ngx_conf_t *cf, ngx_command_t *dummy, void *conf) { char *rv; ngx_str_t *value, file; ngx_cidr_t cidr; ngx_http_geo_conf_ctx_t *ctx; ctx = cf->ctx; value = cf->args->elts; if (cf->args->nelts == 1) { if (ngx_strcmp(value[0].data, "ranges") == 0) { if (ctx->tree) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "the \"ranges\" directive must be " "the first directive inside \"geo\" block"); goto failed; } ctx->high = ngx_pcalloc(ctx->pool, sizeof(ngx_http_geo_high_ranges_t)); if (ctx->high == NULL) { goto failed; } rv = NGX_CONF_OK; goto done; } } if (cf->args->nelts != 2) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid number of the geo parameters"); goto failed; } if (ngx_strcmp(value[0].data, "include") == 0) { file.len = value[1].len++; file.data = ngx_pstrdup(ctx->temp_pool, &value[1]); if (file.data == NULL) { goto failed; } if (ngx_conf_full_name(cf->cycle, &file, 1) != NGX_OK) { goto failed; } ngx_log_debug1(NGX_LOG_DEBUG_CORE, cf->log, 0, "include %s", file.data); rv = ngx_conf_parse(cf, &file); goto done; } else if (ngx_strcmp(value[0].data, "proxy") == 0) { if (ngx_http_geo_cidr_value(cf, &value[1], &cidr) != NGX_OK) { goto failed; } rv = ngx_http_geo_add_proxy(cf, ctx, &cidr); goto done; } if (ctx->high) { rv = ngx_http_geo_range(cf, ctx, value); } else { rv = ngx_http_geo_cidr(cf, ctx, value); } done: ngx_reset_pool(cf->pool); return rv; failed: ngx_reset_pool(cf->pool); return NGX_CONF_ERROR; } static char * ngx_http_geo_range(ngx_conf_t *cf, ngx_http_geo_conf_ctx_t *ctx, ngx_str_t *value) { u_char *p, *last; in_addr_t start, end; ngx_str_t *net; ngx_uint_t del; if (ngx_strcmp(value[0].data, "default") == 0) { if (ctx->high->default_value) { ngx_conf_log_error(NGX_LOG_WARN, cf, 0, "duplicate default geo range value: \"%V\", old value: \"%v\"", &value[1], ctx->high->default_value); } ctx->high->default_value = ngx_http_geo_value(cf, ctx, &value[1]); if (ctx->high->default_value == NULL) { return NGX_CONF_ERROR; } return NGX_CONF_OK; } if (ngx_strcmp(value[0].data, "delete") == 0) { net = &value[1]; del = 1; } else { net = &value[0]; del = 0; } last = net->data + net->len; p = ngx_strlchr(net->data, last, '-'); if (p == NULL) { goto invalid; } start = ngx_inet_addr(net->data, p - net->data); if (start == INADDR_NONE) { goto invalid; } start = ntohl(start); p++; end = ngx_inet_addr(p, last - p); if (end == INADDR_NONE) { goto invalid; } end = ntohl(end); if (start > end) { goto invalid; } if (del) { if (ngx_http_geo_delete_range(cf, ctx, start, end)) { ngx_conf_log_error(NGX_LOG_WARN, cf, 0, "no address range \"%V\" to delete", net); } return NGX_CONF_OK; } ctx->value = ngx_http_geo_value(cf, ctx, &value[1]); if (ctx->value == NULL) { return NGX_CONF_ERROR; } ctx->net = net; return ngx_http_geo_add_range(cf, ctx, start, end); invalid: ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid range \"%V\"", net); return NGX_CONF_ERROR; } /* the add procedure is optimized to add a growing up sequence */ static char * ngx_http_geo_add_range(ngx_conf_t *cf, ngx_http_geo_conf_ctx_t *ctx, in_addr_t start, in_addr_t end) { in_addr_t n; ngx_uint_t h, i, s, e; ngx_array_t *a; ngx_http_geo_range_t *range; for (n = start; n <= end; n = (n + 0x10000) & 0xffff0000) { h = n >> 16; if (n == start) { s = n & 0xffff; } else { s = 0; } if ((n | 0xffff) > end) { e = end & 0xffff; } else { e = 0xffff; } a = (ngx_array_t *) ctx->high->low[h]; if (a == NULL) { a = ngx_array_create(ctx->temp_pool, 64, sizeof(ngx_http_geo_range_t)); if (a == NULL) { return NGX_CONF_ERROR; } ctx->high->low[h] = (ngx_http_geo_range_t *) a; } i = a->nelts; range = a->elts; while (i) { i--; if (e < (ngx_uint_t) range[i].start) { continue; } if (s > (ngx_uint_t) range[i].end) { /* add after the range */ range = ngx_array_push(a); if (range == NULL) { return NGX_CONF_ERROR; } range = a->elts; ngx_memcpy(&range[i + 2], &range[i + 1], (a->nelts - 2 - i) * sizeof(ngx_http_geo_range_t)); range[i + 1].start = (u_short) s; range[i + 1].end = (u_short) e; range[i + 1].value = ctx->value; goto next; } if (s == (ngx_uint_t) range[i].start && e == (ngx_uint_t) range[i].end) { ngx_conf_log_error(NGX_LOG_WARN, cf, 0, "duplicate range \"%V\", value: \"%v\", old value: \"%v\"", ctx->net, ctx->value, range[i].value); range[i].value = ctx->value; goto next; } if (s > (ngx_uint_t) range[i].start && e < (ngx_uint_t) range[i].end) { /* split the range and insert the new one */ range = ngx_array_push(a); if (range == NULL) { return NGX_CONF_ERROR; } range = ngx_array_push(a); if (range == NULL) { return NGX_CONF_ERROR; } range = a->elts; ngx_memcpy(&range[i + 3], &range[i + 1], (a->nelts - 3 - i) * sizeof(ngx_http_geo_range_t)); range[i + 2].start = (u_short) (e + 1); range[i + 2].end = range[i].end; range[i + 2].value = range[i].value; range[i + 1].start = (u_short) s; range[i + 1].end = (u_short) e; range[i + 1].value = ctx->value; range[i].end = (u_short) (s - 1); goto next; } if (s == (ngx_uint_t) range[i].start && e < (ngx_uint_t) range[i].end) { /* shift the range start and insert the new range */ range = ngx_array_push(a); if (range == NULL) { return NGX_CONF_ERROR; } range = a->elts; ngx_memcpy(&range[i + 1], &range[i], (a->nelts - 1 - i) * sizeof(ngx_http_geo_range_t)); range[i + 1].start = (u_short) (e + 1); range[i].start = (u_short) s; range[i].end = (u_short) e; range[i].value = ctx->value; goto next; } if (s > (ngx_uint_t) range[i].start && e == (ngx_uint_t) range[i].end) { /* shift the range end and insert the new range */ range = ngx_array_push(a); if (range == NULL) { return NGX_CONF_ERROR; } range = a->elts; ngx_memcpy(&range[i + 2], &range[i + 1], (a->nelts - 2 - i) * sizeof(ngx_http_geo_range_t)); range[i + 1].start = (u_short) s; range[i + 1].end = (u_short) e; range[i + 1].value = ctx->value; range[i].end = (u_short) (s - 1); goto next; } s = (ngx_uint_t) range[i].start; e = (ngx_uint_t) range[i].end; ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "range \"%V\" overlaps \"%d.%d.%d.%d-%d.%d.%d.%d\"", ctx->net, h >> 8, h & 0xff, s >> 8, s & 0xff, h >> 8, h & 0xff, e >> 8, e & 0xff); return NGX_CONF_ERROR; } /* add the first range */ range = ngx_array_push(a); if (range == NULL) { return NGX_CONF_ERROR; } range->start = (u_short) s; range->end = (u_short) e; range->value = ctx->value; next: continue; } return NGX_CONF_OK; } static ngx_uint_t ngx_http_geo_delete_range(ngx_conf_t *cf, ngx_http_geo_conf_ctx_t *ctx, in_addr_t start, in_addr_t end) { in_addr_t n; ngx_uint_t h, i, s, e, warn; ngx_array_t *a; ngx_http_geo_range_t *range; warn = 0; for (n = start; n <= end; n += 0x10000) { h = n >> 16; if (n == start) { s = n & 0xffff; } else { s = 0; } if ((n | 0xffff) > end) { e = end & 0xffff; } else { e = 0xffff; } a = (ngx_array_t *) ctx->high->low[h]; if (a == NULL) { warn = 1; continue; } range = a->elts; for (i = 0; i < a->nelts; i++) { if (s == (ngx_uint_t) range[i].start && e == (ngx_uint_t) range[i].end) { ngx_memcpy(&range[i], &range[i + 1], (a->nelts - 1 - i) * sizeof(ngx_http_geo_range_t)); a->nelts--; break; } if (s != (ngx_uint_t) range[i].start && e != (ngx_uint_t) range[i].end) { continue; } warn = 1; } } return warn; } static char * ngx_http_geo_cidr(ngx_conf_t *cf, ngx_http_geo_conf_ctx_t *ctx, ngx_str_t *value) { ngx_int_t rc, del; ngx_str_t *net; ngx_uint_t i; ngx_cidr_t cidr; ngx_http_variable_value_t *val, *old; if (ctx->tree == NULL) { ctx->tree = ngx_radix_tree_create(ctx->pool, -1); if (ctx->tree == NULL) { return NGX_CONF_ERROR; } } if (ngx_strcmp(value[0].data, "default") == 0) { cidr.u.in.addr = 0; cidr.u.in.mask = 0; net = &value[0]; } else { if (ngx_strcmp(value[0].data, "delete") == 0) { net = &value[1]; del = 1; } else { net = &value[0]; del = 0; } if (ngx_http_geo_cidr_value(cf, net, &cidr) != NGX_OK) { return NGX_CONF_ERROR; } if (del) { if (ngx_radix32tree_delete(ctx->tree, cidr.u.in.addr, cidr.u.in.mask) != NGX_OK) { ngx_conf_log_error(NGX_LOG_WARN, cf, 0, "no network \"%V\" to delete", net); } return NGX_CONF_OK; } } val = ngx_http_geo_value(cf, ctx, &value[1]); if (val == NULL) { return NGX_CONF_ERROR; } for (i = 2; i; i--) { rc = ngx_radix32tree_insert(ctx->tree, cidr.u.in.addr, cidr.u.in.mask, (uintptr_t) val); if (rc == NGX_OK) { return NGX_CONF_OK; } if (rc == NGX_ERROR) { return NGX_CONF_ERROR; } /* rc == NGX_BUSY */ old = (ngx_http_variable_value_t *) ngx_radix32tree_find(ctx->tree, cidr.u.in.addr & cidr.u.in.mask); ngx_conf_log_error(NGX_LOG_WARN, cf, 0, "duplicate network \"%V\", value: \"%v\", old value: \"%v\"", net, val, old); rc = ngx_radix32tree_delete(ctx->tree, cidr.u.in.addr, cidr.u.in.mask); if (rc == NGX_ERROR) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid radix tree"); return NGX_CONF_ERROR; } } return NGX_CONF_ERROR; } static ngx_http_variable_value_t * ngx_http_geo_value(ngx_conf_t *cf, ngx_http_geo_conf_ctx_t *ctx, ngx_str_t *value) { uint32_t hash; ngx_http_variable_value_t *val; ngx_http_geo_variable_value_node_t *gvvn; hash = ngx_crc32_long(value->data, value->len); gvvn = (ngx_http_geo_variable_value_node_t *) ngx_str_rbtree_lookup(&ctx->rbtree, value, hash); if (gvvn) { return gvvn->value; } val = ngx_palloc(ctx->pool, sizeof(ngx_http_variable_value_t)); if (val == NULL) { return NULL; } val->len = value->len; val->data = ngx_pstrdup(ctx->pool, value); if (val->data == NULL) { return NULL; } val->valid = 1; val->no_cacheable = 0; val->not_found = 0; gvvn = ngx_palloc(ctx->temp_pool, sizeof(ngx_http_geo_variable_value_node_t)); if (gvvn == NULL) { return NULL; } gvvn->sn.node.key = hash; gvvn->sn.str.len = val->len; gvvn->sn.str.data = val->data; gvvn->value = val; ngx_rbtree_insert(&ctx->rbtree, &gvvn->sn.node); return val; } static char * ngx_http_geo_add_proxy(ngx_conf_t *cf, ngx_http_geo_conf_ctx_t *ctx, ngx_cidr_t *cidr) { ngx_in_cidr_t *c; if (ctx->proxies == NULL) { ctx->proxies = ngx_array_create(ctx->pool, 4, sizeof(ngx_in_cidr_t)); if (ctx->proxies == NULL) { return NGX_CONF_ERROR; } } c = ngx_array_push(ctx->proxies); if (c == NULL) { return NGX_CONF_ERROR; } c->addr = cidr->u.in.addr; c->mask = cidr->u.in.mask; return NGX_CONF_OK; } static ngx_int_t ngx_http_geo_cidr_value(ngx_conf_t *cf, ngx_str_t *net, ngx_cidr_t *cidr) { ngx_int_t rc; if (ngx_strcmp(net->data, "255.255.255.255") == 0) { cidr->u.in.addr = 0xffffffff; cidr->u.in.mask = 0xffffffff; return NGX_OK; } rc = ngx_ptocidr(net, cidr); if (rc == NGX_ERROR) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid network \"%V\"", net); return NGX_ERROR; } if (cidr->family != AF_INET) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "\"geo\" supports IPv4 only"); return NGX_ERROR; } if (rc == NGX_DONE) { ngx_conf_log_error(NGX_LOG_WARN, cf, 0, "low address bits of %V are meaningless", net); } cidr->u.in.addr = ntohl(cidr->u.in.addr); cidr->u.in.mask = ntohl(cidr->u.in.mask); return NGX_OK; }