static ngx_int_t
ngx_http_google_response_header_set_cookie(ngx_http_request_t    * r,
                                           ngx_http_google_ctx_t * ctx,
                                           ngx_table_elt_t       * tb)
{
  ngx_array_t * kvs = ngx_http_google_explode_kv(r, &tb->value, ";");
  if (!kvs) return NGX_ERROR;
  
  ngx_uint_t i;
  ngx_keyval_t * kv, * hd = kvs->elts;
  
  for (i = 0; i < kvs->nelts; i++)
  {
    kv = hd + i;
    
    if (!ngx_strncasecmp(kv->key.data, ctx->conf->data, ctx->conf->len))
    {
      if (ngx_http_google_response_header_set_cookie_conf(r, ctx, &kv->value)) {
        return NGX_ERROR;
      }
    }
    
    if (!ngx_strncasecmp(kv->key.data, (u_char *)"GOOGLE_ABUSE_EXEMPTION", 22))
    {
      if (ngx_http_google_response_header_set_cookie_exempt(r, ctx, kvs)) {
        return NGX_ERROR;
      }
    }
    
    if (!ngx_strncasecmp(kv->key.data, (u_char *)"domain", 6))
    {
      kv->value = *ctx->domain;
    }
    
    if (!ngx_strncasecmp(kv->key.data, (u_char *)"path", 4)) {
      ngx_str_set(&kv->value, "/");
    }
  }
  
  // unset this key
  if (!kvs->nelts) {
    tb->hash = 0; return NGX_OK;
  }
  
  ngx_str_t * set_cookie = ngx_http_google_implode_kv(r, kvs, "; ");
  if (!set_cookie) return NGX_ERROR;
  
  // reset set cookie
  tb->value = *set_cookie;
  
  return NGX_OK;
}
static ngx_int_t
ngx_http_google_response_header_set_cookie_conf(ngx_http_request_t    * r,
                                                ngx_http_google_ctx_t * ctx,
                                                ngx_str_t             * v)
{
  if (ctx->type != ngx_http_google_type_main &&
      ctx->type != ngx_http_google_type_scholar) return NGX_OK;
  if (ctx->ncr)                                  return NGX_OK;
  
  ngx_uint_t i;
  ngx_array_t * kvs  = ngx_http_google_explode_kv(r, v, ":");
  if (!kvs) return NGX_ERROR;
  
  ngx_int_t nw = 0;
  ngx_keyval_t * kv, * hd;
  
  hd = kvs->elts;
  for (i = 0; i < kvs->nelts; i++) {
    kv = hd + i;
    if (!ngx_strncasecmp(kv->key.data, (u_char *)"LD", 2)) {
      if (ctx->lang->len) kv->value = *ctx->lang;
      else {
        ngx_str_set(&kv->value, "zh-CN");
      }
    }
    if (!ngx_strncasecmp(kv->key.data, (u_char *)"NW", 2)) nw = 1;
  }
  
  if (!nw) {
    kv = ngx_array_push(kvs);
    if (!kv) return NGX_ERROR;
    ngx_str_set(&kv->key,   "NW");
    ngx_str_set(&kv->value, "1");
  }

  // sort with length
  ngx_sort(kvs->elts, kvs->nelts, sizeof(ngx_keyval_t),
           ngx_http_google_response_header_sort_cookie_conf);
  
  ngx_str_t * nv = ngx_http_google_implode_kv(r, kvs, ":");
  if (!nv) return NGX_ERROR;
  
  *v = *nv;
  
  return NGX_OK;
}
static ngx_int_t
ngx_http_google_request_parser(ngx_http_request_t    * r,
                               ngx_http_google_ctx_t * ctx)
{
  // parse arg
  u_char * last  = r->unparsed_uri.data + r->unparsed_uri.len;
  ctx->arg->data = ngx_strlchr(ctx->uri->data, last, '?');
  
  // parse uri
  if (ctx->arg->data) {
    ctx->arg->len  = last - ++ctx->arg->data;
    ctx->uri->len -= ctx->arg->len + 1;
  }
  
  // parse args
  if (ctx->arg->len) {
    ctx->args = ngx_http_google_explode_kv(r, ctx->arg, "&");
  } else {
    ctx->args = ngx_array_create(r->pool, 4, sizeof(ngx_keyval_t));
  }
  if (!ctx->args) return NGX_ERROR;
  
  // parse cookies
  if (r->headers_in.cookies.nelts) {
    ngx_table_elt_t ** ck = r->headers_in.cookies.elts;
    ctx->cookies = ngx_http_google_explode_kv(r, &(*ck)->value, ";");
  } else {
    ctx->cookies = ngx_array_create(r->pool, 4, sizeof(ngx_keyval_t));
  }
  if (!ctx->cookies) return NGX_ERROR;
  
  // parse host
  if (ngx_http_google_request_parse_host(r, ctx)) return NGX_ERROR;
  
  // traverse headers
  ngx_uint_t i, acl = 0;
  ngx_list_part_t * pt = &r->headers_in.headers.part;
  ngx_table_elt_t * hd = pt->elts, * tb;
  
  for (i = 0; /* void */; i++)
  {
    if (i >= pt->nelts) {
      
      if (pt->next == NULL) break;
      
      pt = pt->next;
      hd = pt->elts;
      i  = 0;
    }
    
    tb = hd + i;
    if (!ngx_strncasecmp(tb->key.data, (u_char *)"Accept-Language", 15)) {
      acl = 1; tb->value = *ctx->lang;
    }
  }
  
  if (!acl) {
    tb = ngx_list_push(&r->headers_in.headers);
    if (!tb) return NGX_ERROR;
    
    ngx_str_set(&tb->key, "Accept-Language");
    tb->value = *ctx->lang;
    tb->hash  = ngx_hash_key_lc(tb->key.data, tb->key.len);
  }
  
  return NGX_OK;
}