leevis.com icon indicating copy to clipboard operation
leevis.com copied to clipboard

nginx proxy 模块请求发往上游

Open vislee opened this issue 7 years ago • 0 comments

概述

nginx通过proxy_pass url; 来指定一组上游服务器,来实现7层http的反向代理功能。 通过URL指定一组上游服务器,URL可以是变量、域名、upstream的配置名称。

server {
    ......

    set $ups "127.0.0.1:8990";
    location /test1/ {
        proxy_pass http://$ups/;
    }

    location /test2/ {
        proxy_pass http://$ups;
    }

    location /test3/ {
        proxy_pass http://127.0.0.1:8991/;
    }

    location /test4/ {
        proxy_pass http://127.0.0.1:8991;
    }

    location /test5/ {
        proxy_pass http://127.0.0.1:8991/www/;
    }
}

对照上述4种配置的测试结果:

# curl -v 'http://127.0.0.1:8080/test1/hello/liwq'
$nc -l 127.0.0.1 8990
GET / HTTP/1.0

# curl -v 'http://127.0.0.1:8080/test2/hello/liwq'
$nc -l 127.0.0.1 8990
GET /test2/hello/liwq HTTP/1.0

#curl -v 'http://127.0.0.1:8080/test3/hello/liwq'
$nc -l 127.0.0.1 8991
GET /hello/liwq HTTP/1.0

#curl -v 'http://127.0.0.1:8080/test4/hello/liwq'
$nc -l 127.0.0.1 8991
GET /test4/hello/liwq HTTP/1.0

#curl -v 'http://127.0.0.1:8080/test5/hello/liwq'
$nc -l 127.0.0.1 8991
GET /www/hello/liwq HTTP/1.0

小结: 根据proxy_pass后的URL中是否有变量和是否有uri,转到上游的url不同:

  1. 有变量有uri:转到上游的请求url是proxy_pass 的URL中的uri。
  2. 有变量无uri:转到上游的请求url是原来请求的url。
  3. 无变量有uri:转到上游的请求url是proxy_pass 的URL中的uri + 原来请求去掉location 的name剩下的url。
  4. 无变量无uri:转到上游的请求url是原来请求的url。

代码解析

配置解析

下面通过解析proxy模块的proxy_pass指令来看看,proxy模块是如何选择上游服务器的,又是如何拼接请求把请求发往上游服务器的。下面介绍proxy_pass指令对应的代码ngx_http_proxy_pass函数。

  • 通过检测plcf->upstream.upstream || plcf->proxy_lengths 来判断是否在一个location重复配置proxy_pass指令。
  • 赋值该location下content阶段的处理函数为ngx_http_proxy_handler。
  • 判断URL是否有变量:
    • 有变量则调用ngx_http_script_compile函数把变量的回调函数添加到plcf->proxy_lengths和plcf->proxy_values数组中。
    • 没有变量,则调用ngx_http_upstream_add查找一组上游配置。

通过配置指令proxy_set_header设置一组发到上游的header,nginx代码是通过ngx_conf_set_keyval_slot函数把key val插入到plcf->headers_source数组中,数组的元素是ngx_keyval_t。 在merge_loc_conf 回调函数中,通过调用ngx_http_proxy_init_headers函数初始header。

ngx_http_proxy_init_headers 函数解析:

  • 分配了bucket,说明已经初始化完成了,不用再次初始化了。
  • 初始化headers_names为hash表,headers_merged为header 的key val结构。
  • 合并headers_source和default_headers到headers_merged数组中,两个数组中有相同的元素,取headers_source。
  • 遍历合并后的headers_merged数组,初始化hash表初始化所用的headers_names 数组。同时把合并数组中的header的长度以及回调函数存到headers->lengths数组中,内容存以及回调函数存到headers->values数组中,因涉及到变量,所以比较复杂。lengths数组中的元素是ngx_http_script_copy_code_t结构体,values数组中的元素是ngx_http_script_copy_code_t结构体,和该结构体后边紧跟的内容。如果header的val没有变量,则一个数组元素对应一个header,否则多个数组元素才对应一个header,两个header在数组中会用null隔开。val中变量会调用ngx_http_script_compile函数编译。
  • 编译headers->hash。
typedef struct {
    ngx_array_t                   *flushes;
    ngx_array_t                   *lengths;
    ngx_array_t                   *values;   // proxy_set_header的val支持变量
    ngx_hash_t                     hash;     // 配置文件中通过proxy_set_header设置的回源请求头和nginx默认的请求头
} ngx_http_proxy_headers_t;

static ngx_int_t
ngx_http_proxy_init_headers(ngx_conf_t *cf, ngx_http_proxy_loc_conf_t *conf,
    ngx_http_proxy_headers_t *headers, ngx_keyval_t *default_headers)
{
    u_char                       *p;
    size_t                        size;
    uintptr_t                    *code;
    ngx_uint_t                    i;
    ngx_array_t                   headers_names, headers_merged;
    ngx_keyval_t                 *src, *s, *h;
    ngx_hash_key_t               *hk;
    ngx_hash_init_t               hash;
    ngx_http_script_compile_t     sc;
    ngx_http_script_copy_code_t  *copy;

    if (headers->hash.buckets) {
        return NGX_OK;
    }

    // 构建hash表使用
    if (ngx_array_init(&headers_names, cf->temp_pool, 4, sizeof(ngx_hash_key_t))
        != NGX_OK)
    {
        return NGX_ERROR;
    }

    // 配置文件proxy_set_header设置的头和nginx默认的头合并,
    // 如果配置文件和默认的有冲突,取配置文件的。
    if (ngx_array_init(&headers_merged, cf->temp_pool, 4, sizeof(ngx_keyval_t))
        != NGX_OK)
    {
        return NGX_ERROR;
    }

    // 回源请求头长度
    headers->lengths = ngx_array_create(cf->pool, 64, 1);
    if (headers->lengths == NULL) {
        return NGX_ERROR;
    }
    // 回源请求头内容
    headers->values = ngx_array_create(cf->pool, 512, 1);
    if (headers->values == NULL) {
        return NGX_ERROR;
    }

    // 通过proxy_set_header设置的请求头
    // 复制到headers_merged数组中
    if (conf->headers_source) {

        src = conf->headers_source->elts;
        for (i = 0; i < conf->headers_source->nelts; i++) {

            s = ngx_array_push(&headers_merged);
            if (s == NULL) {
                return NGX_ERROR;
            }

            *s = src[i];
        }
    }

    // 再把默认的请求头复制到headers_merged数组中,如果headers_merged已经有的则跳过。
    h = default_headers;

    while (h->key.len) {

        src = headers_merged.elts;
        for (i = 0; i < headers_merged.nelts; i++) {
            if (ngx_strcasecmp(h->key.data, src[i].key.data) == 0) {
                goto next;
            }
        }

        s = ngx_array_push(&headers_merged);
        if (s == NULL) {
            return NGX_ERROR;
        }

        *s = *h;

    next:

        h++;
    }


    src = headers_merged.elts;
    for (i = 0; i < headers_merged.nelts; i++) {

        hk = ngx_array_push(&headers_names);
        if (hk == NULL) {
            return NGX_ERROR;
        }

        hk->key = src[i].key;
        hk->key_hash = ngx_hash_key_lc(src[i].key.data, src[i].key.len);
        hk->value = (void *) 1;

        if (src[i].value.len == 0) {
            continue;
        }

        copy = ngx_array_push_n(headers->lengths,
                                sizeof(ngx_http_script_copy_code_t));
        if (copy == NULL) {
            return NGX_ERROR;
        }

        copy->code = (ngx_http_script_code_pt) ngx_http_script_copy_len_code;
        copy->len = src[i].key.len;

        size = (sizeof(ngx_http_script_copy_code_t)
                + src[i].key.len + sizeof(uintptr_t) - 1)
               & ~(sizeof(uintptr_t) - 1);

        copy = ngx_array_push_n(headers->values, size);
        if (copy == NULL) {
            return NGX_ERROR;
        }

        copy->code = ngx_http_script_copy_code;
        copy->len = src[i].key.len;

        p = (u_char *) copy + sizeof(ngx_http_script_copy_code_t);
        ngx_memcpy(p, src[i].key.data, src[i].key.len);

        ngx_memzero(&sc, sizeof(ngx_http_script_compile_t));

        sc.cf = cf;
        sc.source = &src[i].value;
        sc.flushes = &headers->flushes;
        sc.lengths = &headers->lengths;
        sc.values = &headers->values;

        if (ngx_http_script_compile(&sc) != NGX_OK) {
            return NGX_ERROR;
        }

        code = ngx_array_push_n(headers->lengths, sizeof(uintptr_t));
        if (code == NULL) {
            return NGX_ERROR;
        }

        *code = (uintptr_t) NULL;

        code = ngx_array_push_n(headers->values, sizeof(uintptr_t));
        if (code == NULL) {
            return NGX_ERROR;
        }

        *code = (uintptr_t) NULL;
    }

    code = ngx_array_push_n(headers->lengths, sizeof(uintptr_t));
    if (code == NULL) {
        return NGX_ERROR;
    }

    *code = (uintptr_t) NULL;


    hash.hash = &headers->hash;
    hash.key = ngx_hash_key_lc;
    hash.max_size = conf->headers_hash_max_size;
    hash.bucket_size = conf->headers_hash_bucket_size;
    hash.name = "proxy_headers_hash";
    hash.pool = cf->pool;
    hash.temp_pool = NULL;

    return ngx_hash_init(&hash, headers_names.elts, headers_names.nelts);
}

请求解析

下游请求到达时,解析完请求头后调用11个阶段,当调用到content阶段的时候执行ngx_http_proxy_handler回调函数。

  • 调用ngx_http_upstream_create函数创建一个upstream的结构体,赋值给r->upstream;
  • 创建proxy模块的请求上下文,其类型为ngx_http_proxy_ctx_t结构体。
  • 判断plcf->proxy_lengths数组是否为null
    • 如果为null,则说明proxy_pass的url没有变量。
    • 如果不为null,则说明url有变量,调用ngx_http_proxy_eval。
  • 赋值upstream结构体回调函数和变量,r->request_body_no_buffering为真表示不换成request,u->buffering为真表示缓存resp。
  • 调用ngx_http_read_client_request_body函数读取完body后,再调用ngx_http_upstream_init函数初始化上游链接。

全部读取完下游请求后调用ngx_http_upstream_init初始化上游链接,实际上是调用的ngx_http_upstream_init_request函数。

  • 如果不保存上游结果且不忽略客户端异常且post_action为空,则赋值request的读写回调:

      r->read_event_handler = ngx_http_upstream_rd_check_broken_connection;
      r->write_event_handler = ngx_http_upstream_wr_check_broken_connection;
    
  • 客户端请求有body,则赋值:u->request_bufs = r->request_body->bufs;

  • 调用u->create_request回调函数创建发送到上游的请求体。其回调函数为:ngx_http_proxy_create_request

    • 计算发往上游请求的长度(方法长度+http版本长度+回车换行长度+uri的长度+[配置的body的长度]+ 配置的请求头的长度+原请求请求头的长度),注意:如果没有配置proxy_set_body,且proxy_pass_request_body为on,发到上游的body是原请求的body,原请求的body在r->request_body->bufs中,已经赋值给u->request_bufs。
    • 分配请求长度的内存,用来构建发往上游的请求。
    • plcf->proxy_lengths && ctx->vars.uri.len proxy_pass的URL有变量也有uri。则发到上游的uri为URL配置的uri。
    • URL中没有配置uri,解析客户端的请求uri有效且不是子请求,则发到上游的uri为原客户端的uri。
    • 是有效的location且proxy_pass的URL有uri,则发往上游的请求uri是proxy_pass的URL的uri+原请求的uri减去location name前缀再拼上原请求的参数。否则,发往上游请求的url是原客户端的url。
    • 请求头中,val为空的key val会被丢弃。
    • 如果没有配置proxy_set_body,且proxy_pass_request_body为on,且原请求有body,则把body挂到组装好的请求的内存链后边。
    • u->request_bufs指向发往上游的请求头+请求体。
  • uscf->peer.init

  • 调用ngx_http_upstream_connect函数向上游发起链接请求。

// 解析指令 proxy_pass URL; 中的URL保存到vars结构体。
typedef struct {
    ngx_str_t                      key_start;       // 指向uri
    ngx_str_t                      schema;         // 是http还是https协议
    ngx_str_t                      host_header; //  ip:port 或host
    ngx_str_t                      port;               // 指定的端口
    ngx_str_t                      uri;                  // 指定的uri
} ngx_http_proxy_vars_t;

typedef struct {
    ngx_http_status_t              status;
    ngx_http_chunked_t             chunked;
    ngx_http_proxy_vars_t          vars;
    // 通过该指令proxy_set_body 设置的body的长度 或者 原请求body长度。
    off_t                          internal_body_length;

    ngx_chain_t                   *free;
    ngx_chain_t                   *busy;

    unsigned                       head:1;
    unsigned                       internal_chunked:1;
    unsigned                       header_sent:1;
} ngx_http_proxy_ctx_t;


static ngx_int_t
ngx_http_proxy_create_request(ngx_http_request_t *r)
{
    size_t                        len, uri_len, loc_len, body_len,
                                  key_len, val_len;
    uintptr_t                     escape;
    ngx_buf_t                    *b;
    ngx_str_t                     method;
    ngx_uint_t                    i, unparsed_uri;
    ngx_chain_t                  *cl, *body;
    ngx_list_part_t              *part;
    ngx_table_elt_t              *header;
    ngx_http_upstream_t          *u;
    ngx_http_proxy_ctx_t         *ctx;
    ngx_http_script_code_pt       code;
    ngx_http_proxy_headers_t     *headers;
    ngx_http_script_engine_t      e, le;
    ngx_http_proxy_loc_conf_t    *plcf;
    ngx_http_script_len_code_pt   lcode;

    u = r->upstream;

    plcf = ngx_http_get_module_loc_conf(r, ngx_http_proxy_module);

#if (NGX_HTTP_CACHE)
    headers = u->cacheable ? &plcf->headers_cache : &plcf->headers;
#else
    headers = &plcf->headers;
#endif

    if (u->method.len) {
        /* HEAD was changed to GET to cache response */
        method = u->method;

    } else if (plcf->method) {
        if (ngx_http_complex_value(r, plcf->method, &method) != NGX_OK) {
            return NGX_ERROR;
        }

    } else {
        method = r->method_name;
    }

    ctx = ngx_http_get_module_ctx(r, ngx_http_proxy_module);

    if (method.len == 4
        && ngx_strncasecmp(method.data, (u_char *) "HEAD", 4) == 0)
    {
        ctx->head = 1;
    }

    //  GET uri HTTP/1.0
    //  ngx_http_proxy_version 是定义的字符串常量,以0结尾,所以减去1。 CRLF同理。
    len = method.len + 1 + sizeof(ngx_http_proxy_version) - 1
          + sizeof(CRLF) - 1;

    escape = 0;
    loc_len = 0;
    unparsed_uri = 0;

    if (plcf->proxy_lengths && ctx->vars.uri.len) {
       // proxy_pass 指令的URL有变量   and  URL指定了uri
        uri_len = ctx->vars.uri.len;

    } else if (ctx->vars.uri.len == 0 && r->valid_unparsed_uri && r == r->main)
    {        //  proxy_pass 没有指定uri  and  请求的uri没有编码  and 原始请求
        unparsed_uri = 1;
        uri_len = r->unparsed_uri.len;

    } else {
        // location 有效and proxy_pass 指定了uri 则 
        // 取location name的长度(proxy_pass 指令所属location 的name)
        loc_len = (r->valid_location && ctx->vars.uri.len) ?
                      plcf->location.len : 0;

        if (r->quoted_uri || r->space_in_uri || r->internal) {
            escape = 2 * ngx_escape_uri(NULL, r->uri.data + loc_len,
                                        r->uri.len - loc_len, NGX_ESCAPE_URI);
        }

        // proxy_pass 指定uri的长度 + 原请求uri减去location name的长度 + encode添加额外字符的长度 
        // + ? + args 参数长度。
        uri_len = ctx->vars.uri.len + r->uri.len - loc_len + escape
                  + sizeof("?") - 1 + r->args.len;
    }

    if (uri_len == 0) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "zero length URI to proxy");
        return NGX_ERROR;
    }

    len += uri_len;

    ngx_memzero(&le, sizeof(ngx_http_script_engine_t));

    ngx_http_script_flush_no_cacheable_variables(r, plcf->body_flushes);
    ngx_http_script_flush_no_cacheable_variables(r, headers->flushes);

    if (plcf->body_lengths) {
        le.ip = plcf->body_lengths->elts;
        le.request = r;
        le.flushed = 1;
        body_len = 0;

        while (*(uintptr_t *) le.ip) {
            lcode = *(ngx_http_script_len_code_pt *) le.ip;
            body_len += lcode(&le);
        }

        ctx->internal_body_length = body_len;
        len += body_len;

    } else if (r->headers_in.chunked && r->reading_body) {
        ctx->internal_body_length = -1;
        ctx->internal_chunked = 1;

    } else {
        ctx->internal_body_length = r->headers_in.content_length_n;
    }

    le.ip = headers->lengths->elts;
    le.request = r;
    le.flushed = 1;

    while (*(uintptr_t *) le.ip) {

        lcode = *(ngx_http_script_len_code_pt *) le.ip;
        key_len = lcode(&le);

        // 每组请求头之间通过NULL指针分割
        for (val_len = 0; *(uintptr_t *) le.ip; val_len += lcode(&le)) {
            lcode = *(ngx_http_script_len_code_pt *) le.ip;
        }
        // 跳过分割的空指针,到下一组请求头
        le.ip += sizeof(uintptr_t);

        // 如果对应请求头的值为空,则跳过该请求头
        if (val_len == 0) {
            continue;
        }
        // 添加一组请求头的长度 key: value
        len += key_len + sizeof(": ") - 1 + val_len + sizeof(CRLF) - 1;
    }

    // 原请求的头是否可以转发到后端
    if (plcf->upstream.pass_request_headers) {
        part = &r->headers_in.headers.part;
        header = part->elts;

        for (i = 0; /* void */; i++) {

            if (i >= part->nelts) {
                if (part->next == NULL) {
                    break;
                }

                part = part->next;
                header = part->elts;
                i = 0;
            }

            // ngx_http_proxy_headers数组和通过proxy_set_header指令指定的头会覆盖原请求的头
            if (ngx_hash_find(&headers->hash, header[i].hash,
                              header[i].lowcase_key, header[i].key.len))
            {
                continue;
            }

            len += header[i].key.len + sizeof(": ") - 1
                + header[i].value.len + sizeof(CRLF) - 1;
        }
    }


    b = ngx_create_temp_buf(r->pool, len);
    if (b == NULL) {
        return NGX_ERROR;
    }

    cl = ngx_alloc_chain_link(r->pool);
    if (cl == NULL) {
        return NGX_ERROR;
    }

    cl->buf = b;

    // 以上只是计算长度为了分配内存。以下会拼接发往上游的请求
    /* the request line */

    b->last = ngx_copy(b->last, method.data, method.len);
    *b->last++ = ' ';

    u->uri.data = b->last;
    // 以下是拼接请求的uri
    if (plcf->proxy_lengths && ctx->vars.uri.len) {
        // proxy_pass  的URI 有变量。转发到上游的uri 只解析proxy_pass 后的。
        b->last = ngx_copy(b->last, ctx->vars.uri.data, ctx->vars.uri.len);

    } else if (unparsed_uri) {
        b->last = ngx_copy(b->last, r->unparsed_uri.data, r->unparsed_uri.len);

    } else {
        if (r->valid_location) {
            b->last = ngx_copy(b->last, ctx->vars.uri.data, ctx->vars.uri.len);
        }

        if (escape) {
            ngx_escape_uri(b->last, r->uri.data + loc_len,
                           r->uri.len - loc_len, NGX_ESCAPE_URI);
            b->last += r->uri.len - loc_len + escape;

        } else {
            // 拼接原请求uri移除location name的前缀
            b->last = ngx_copy(b->last, r->uri.data + loc_len,
                               r->uri.len - loc_len);
        }
        // 拼接参数。。。
        if (r->args.len > 0) {
            *b->last++ = '?';
            b->last = ngx_copy(b->last, r->args.data, r->args.len);
        }
    }

    u->uri.len = b->last - u->uri.data;

    // 如果没有通过proxy_http_version指定http版本,则默认会选择http1.0
    if (plcf->http_version == NGX_HTTP_VERSION_11) {
        b->last = ngx_cpymem(b->last, ngx_http_proxy_version_11,
                             sizeof(ngx_http_proxy_version_11) - 1);

    } else {
        b->last = ngx_cpymem(b->last, ngx_http_proxy_version,
                             sizeof(ngx_http_proxy_version) - 1);
    }

    ngx_memzero(&e, sizeof(ngx_http_script_engine_t));

    e.ip = headers->values->elts;
    e.pos = b->last;
    e.request = r;
    e.flushed = 1;
    // 重置计算header头的长度
    le.ip = headers->lengths->elts;

    while (*(uintptr_t *) le.ip) {

        lcode = *(ngx_http_script_len_code_pt *) le.ip;
        // 跳过key
        (void) lcode(&le);
        // 计算val的长度
        for (val_len = 0; *(uintptr_t *) le.ip; val_len += lcode(&le)) {
            lcode = *(ngx_http_script_len_code_pt *) le.ip;
        }
        le.ip += sizeof(uintptr_t);

        if (val_len == 0) {
            // 对应的key没有值,则跳过。不拼接该header
            e.skip = 1;

            while (*(uintptr_t *) e.ip) {
                code = *(ngx_http_script_code_pt *) e.ip;
                code((ngx_http_script_engine_t *) &e);
            }
            e.ip += sizeof(uintptr_t);

            e.skip = 0;

            continue;
        }

        code = *(ngx_http_script_code_pt *) e.ip;
        code((ngx_http_script_engine_t *) &e);

        *e.pos++ = ':'; *e.pos++ = ' ';

        while (*(uintptr_t *) e.ip) {
            code = *(ngx_http_script_code_pt *) e.ip;
            code((ngx_http_script_engine_t *) &e);
        }
        e.ip += sizeof(uintptr_t);

        *e.pos++ = CR; *e.pos++ = LF;
    }

    b->last = e.pos;


    if (plcf->upstream.pass_request_headers) {
        part = &r->headers_in.headers.part;
        header = part->elts;

        for (i = 0; /* void */; i++) {

            if (i >= part->nelts) {
                if (part->next == NULL) {
                    break;
                }

                part = part->next;
                header = part->elts;
                i = 0;
            }

            if (ngx_hash_find(&headers->hash, header[i].hash,
                              header[i].lowcase_key, header[i].key.len))
            {
                continue;
            }

            b->last = ngx_copy(b->last, header[i].key.data, header[i].key.len);

            *b->last++ = ':'; *b->last++ = ' ';

            b->last = ngx_copy(b->last, header[i].value.data,
                               header[i].value.len);

            *b->last++ = CR; *b->last++ = LF;

            ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                           "http proxy header: \"%V: %V\"",
                           &header[i].key, &header[i].value);
        }
    }


    /* add "\r\n" at the header end */
    *b->last++ = CR; *b->last++ = LF;

    if (plcf->body_values) {
        e.ip = plcf->body_values->elts;
        e.pos = b->last;
        e.skip = 0;

        while (*(uintptr_t *) e.ip) {
            code = *(ngx_http_script_code_pt *) e.ip;
            code((ngx_http_script_engine_t *) &e);
        }

        b->last = e.pos;
    }

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "http proxy header:%N\"%*s\"",
                   (size_t) (b->last - b->pos), b->pos);


    if (r->request_body_no_buffering) {
       // 原请求的body不在内存中
        u->request_bufs = cl;

        if (ctx->internal_chunked) {
            // u->output 是向上游发起请求
            // 原请求是chunked类型的
            u->output.output_filter = ngx_http_proxy_body_output_filter;
            u->output.filter_ctx = r;
        }

    } else if (plcf->body_values == NULL && plcf->upstream.pass_request_body) {

        body = u->request_bufs;
        u->request_bufs = cl;

        while (body) {
            b = ngx_alloc_buf(r->pool);
            if (b == NULL) {
                return NGX_ERROR;
            }

            ngx_memcpy(b, body->buf, sizeof(ngx_buf_t));

            cl->next = ngx_alloc_chain_link(r->pool);
            if (cl->next == NULL) {
                return NGX_ERROR;
            }

            cl = cl->next;
            cl->buf = b;

            body = body->next;
        }

    } else {
        u->request_bufs = cl;
    }

    b->flush = 1;
    cl->next = NULL;

    return NGX_OK;
}

vislee avatar Apr 06 '17 07:04 vislee