| /* ==================================================================== |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * ==================================================================== |
| */ |
| |
| #include <stdlib.h> |
| |
| #include <apr_pools.h> |
| |
| #include "serf.h" |
| #include "serf_bucket_util.h" |
| #include "serf_private.h" |
| #include "protocols/http2_buckets.h" |
| |
| #include "hpack_huffman.inc" |
| #define EOS_CHAR (256) |
| |
| /* Callback for bsearch() */ |
| static int |
| hpack_hm_compare(const void *k, |
| const void *i) |
| { |
| const apr_uint32_t *key = k; |
| const struct serf_hpack_huffman_item_t *it = i; |
| |
| apr_uint32_t k1 = (*key & it->hmask); |
| apr_uint32_t k2 = it->hval; |
| |
| if (k1 < k2) |
| return -1; |
| else if (k1 > k2) |
| return 1; |
| else |
| return 0; |
| } |
| |
| /* Convert encoded data in ENCODED of size ENCODED_LEN. If TEXT is not |
| NULL, put the result in the buffer pointed to by TEXT which is of size |
| TEXT_AVAIL. Sets *TEXT_LEN to the resulting length. |
| |
| If TEXT_AVAIL allows it a final '\0' is added at TEXT[*TEXT_LEN]. |
| |
| If TEXT is not large enough return APR_ENOMEM. If ENCODED isn't properly |
| encoded return APR_EINVAL. |
| */ |
| apr_status_t |
| serf__hpack_huffman_decode(const unsigned char *encoded, |
| apr_size_t encoded_len, |
| apr_size_t text_avail, |
| char *text, |
| apr_size_t *text_len) |
| { |
| apr_uint64_t stash = 0; |
| apr_int16_t bits_left = 0; |
| apr_size_t result_len = 0; |
| |
| while (encoded_len || bits_left) |
| { |
| apr_uint32_t match; |
| struct serf_hpack_huffman_item_t *r; |
| |
| while (bits_left < 30 && encoded_len) |
| { |
| stash |= (apr_uint64_t)*encoded << (64 - 8 - bits_left); |
| bits_left += 8; |
| encoded_len--; |
| encoded++; |
| } |
| |
| match = stash >> 32; |
| r = bsearch(&match, &serf_hpack_hm_map, |
| sizeof(serf_hpack_hm_map) / sizeof(serf_hpack_hm_map[0]), |
| sizeof(serf_hpack_hm_map[0]), hpack_hm_compare); |
| |
| if (!r || (r->bits > bits_left)) |
| { |
| /* With a canonical huffman code we can only reach this state |
| at the end of the string */ |
| break; |
| } |
| |
| if (r->cval == EOS_CHAR) |
| return APR_EINVAL; |
| |
| if (text) |
| { |
| if (result_len < text_avail) |
| text[result_len] = (char)r->cval; |
| else |
| return APR_ENOMEM; |
| } |
| |
| result_len++; |
| stash <<= r->bits; |
| bits_left -= r->bits; |
| } |
| |
| if (bits_left) |
| { |
| /* https://tools.ietf.org/html/rfc7541#section-5.2 |
| |
| Upon decoding, an incomplete code at the end of the encoded data is |
| to be considered as padding and discarded. A padding strictly longer |
| than 7 bits MUST be treated as a decoding error. A padding not |
| corresponding to the most significant bits of the code for the EOS |
| symbol MUST be treated as a decoding error. A Huffman-encoded string |
| literal containing the EOS symbol MUST be treated as a decoding |
| error. */ |
| const struct serf_hpack_huffman_item_t *eos; |
| apr_uint64_t exp_stash; |
| |
| eos = &serf_hpack_hm_map[serf_hpack_hm_rmap[EOS_CHAR]]; |
| |
| /* Trim EOS value to the bits we need */ |
| exp_stash = ((apr_uint32_t)eos->hval >> (32 - bits_left)); |
| /* And move it to the right position */ |
| exp_stash <<= 64 - bits_left; |
| |
| if (exp_stash != stash) |
| return APR_EINVAL; |
| } |
| |
| *text_len = result_len; |
| if (text && result_len < text_avail) |
| text[result_len] = 0; |
| |
| return APR_SUCCESS; |
| } |
| |
| /* Encodes data in TEXT of size TEXT_LEN. |
| |
| Sets ENCODED_LEN to the required length. |
| |
| If ENCODED is not NULL, it specifies an output buffer of size |
| ENCODED_AVAIL into which the data will be encoded. |
| |
| If ENCODE is not NULL and the data doesn't fit returns APR_ENOMEM. |
| */ |
| apr_status_t |
| serf__hpack_huffman_encode(const char *text, |
| apr_size_t text_len, |
| apr_size_t encoded_avail, |
| unsigned char *encoded, |
| apr_size_t *encoded_len) |
| { |
| apr_uint64_t stash = 0; |
| apr_int16_t bits_left = 0; |
| apr_size_t result_len = 0; |
| |
| if (!encoded) |
| { |
| /* Just calculating size needed. Avoid bit handling */ |
| apr_int64_t result_bits = 0; |
| while (text_len) |
| { |
| const struct serf_hpack_huffman_item_t *r; |
| |
| r = &serf_hpack_hm_map[serf_hpack_hm_rmap[*(unsigned char*)text]]; |
| |
| result_bits += r->bits; |
| text_len--; |
| text++; |
| } |
| |
| *encoded_len = (apr_size_t)((result_bits + 7) / 8); |
| return APR_SUCCESS; |
| } |
| |
| while (text_len) |
| { |
| if (text_len) |
| { |
| const struct serf_hpack_huffman_item_t *r; |
| |
| r = &serf_hpack_hm_map[serf_hpack_hm_rmap[*(unsigned char*)text]]; |
| |
| stash |= (apr_uint64_t)r->hval << (64 - 32 - bits_left); |
| bits_left += r->bits; |
| text_len--; |
| text++; |
| } |
| |
| while (bits_left > 8) |
| { |
| if (!encoded_avail) |
| return APR_ENOMEM; |
| |
| *encoded = (unsigned char)(stash >> (64 - 8)); |
| encoded++; |
| stash <<= 8; |
| bits_left -= 8; |
| |
| encoded_avail--; |
| result_len++; |
| } |
| } |
| |
| if (bits_left) |
| { |
| /* https://tools.ietf.org/html/rfc7541#section-5.2 |
| |
| As the Huffman-encoded data doesn't always end at an octet boundary, |
| some padding is inserted after it, up to the next octet boundary. To |
| prevent this padding from being misinterpreted as part of the string |
| literal, the most significant bits of the code corresponding to the |
| EOS (end-of-string) symbol are used. */ |
| const struct serf_hpack_huffman_item_t *r; |
| |
| if (!encoded_avail) |
| return APR_ENOMEM; |
| |
| r = &serf_hpack_hm_map[serf_hpack_hm_rmap[EOS_CHAR]]; |
| stash |= (apr_uint64_t)r->hval << (64 - 32 - bits_left); |
| /* bits_left += r->bits; */ |
| |
| *encoded = (unsigned char)(stash >> (64 - 8)); |
| /* encoded++ */ |
| /* stash <<= 8; */ |
| /* bits_left -= 8; */ |
| /* encoded_avail--; */ |
| result_len++; |
| } |
| |
| *encoded_len = result_len; |
| |
| return APR_SUCCESS; |
| } |
| |
| /* ==================================================================== */ |
| |
| typedef struct serf_hpack_entry_t |
| { |
| const char *key; |
| apr_size_t key_len; |
| const char *value; |
| apr_size_t value_len; |
| |
| struct serf_hpack_entry_t *next; |
| struct serf_hpack_entry_t *prev; |
| |
| bool free_key; /* Key must be freed */ |
| bool free_val; /* Value must be freed */ |
| char dont_index; /* 0=index, 1=no-index, 2=never-index */ |
| } serf_hpack_entry_t; |
| |
| static void hpack_free_entry(serf_hpack_entry_t *entry, |
| serf_bucket_alloc_t *alloc) |
| { |
| if (entry->free_key) |
| serf_bucket_mem_free(alloc, (char*)entry->key); |
| if (entry->free_val) |
| serf_bucket_mem_free(alloc, (char*)entry->value); |
| serf_bucket_mem_free(alloc, entry); |
| } |
| |
| /* https://tools.ietf.org/html/rfc7541#section-4.1 |
| |
| The size of an entry is the sum of its name's length in octets (as |
| defined in Section 5.2), its value's length in octets, and 32. |
| |
| The size of an entry is calculated using the length of its name and |
| value without any Huffman encoding applied. |
| */ |
| #define HPACK_ENTRY_SIZE(hi) ((hi)->key_len + (hi)->value_len + 32) |
| /* The per key, value variant of HPACK_ENTRY_SIZE */ |
| #define HPACK_KEY_SIZE(key_sz) ((key_sz) + 16) |
| |
| struct serf_hpack_table_t |
| { |
| apr_pool_t *pool; |
| serf_bucket_alloc_t *alloc; |
| |
| char lowercase_keys; |
| char send_tablesize_update; |
| |
| /* The local -> remote 'encoder' list */ |
| serf_hpack_entry_t *lr_first, *lr_last; |
| apr_size_t lr_size; /* 'Bytes' in list, calculated by HPACK_ENTRY_SIZE() */ |
| apr_size_t lr_max_table_size; |
| apr_size_t lr_sys_table_size; |
| |
| serf_hpack_entry_t *rl_first, *rl_last; |
| apr_size_t rl_size; /* 'Bytes' in list, calculated by HPACK_ENTRY_SIZE() */ |
| apr_size_t rl_max_table_size; |
| apr_size_t rl_sys_table_size; |
| }; |
| |
| /* The staticly defined list of pre-encoded entries. All numbers above |
| this list are dynamically defined, so some new standard is needed to |
| extend this list */ |
| static const serf_hpack_entry_t hpack_static_table[] = |
| { |
| #define HPACK_STR(x) x, (sizeof(x)-1) |
| {/* 1*/ HPACK_STR(":authority"), HPACK_STR("")}, |
| {/* 2*/ HPACK_STR(":method"), HPACK_STR("GET")}, |
| {/* 3*/ HPACK_STR(":method"), HPACK_STR("POST")}, |
| {/* 4*/ HPACK_STR(":path"), HPACK_STR("/")}, |
| {/* 5*/ HPACK_STR(":path"), HPACK_STR("/index.html") }, |
| {/* 6*/ HPACK_STR(":scheme"), HPACK_STR("http")}, |
| {/* 7*/ HPACK_STR(":scheme"), HPACK_STR("https")}, |
| {/* 8*/ HPACK_STR(":status"), HPACK_STR("200")}, |
| {/* 9*/ HPACK_STR(":status"), HPACK_STR("204")}, |
| {/*10*/ HPACK_STR(":status"), HPACK_STR("206")}, |
| {/*11*/ HPACK_STR(":status"), HPACK_STR("304")}, |
| {/*12*/ HPACK_STR(":status"), HPACK_STR("400")}, |
| {/*13*/ HPACK_STR(":status"), HPACK_STR("404")}, |
| {/*14*/ HPACK_STR(":status"), HPACK_STR("500")}, |
| {/*15*/ HPACK_STR("accept-charset"), HPACK_STR("")}, |
| {/*16*/ HPACK_STR("accept-encoding"), HPACK_STR("gzip, deflate")}, |
| {/*17*/ HPACK_STR("accept-language"), HPACK_STR("")}, |
| {/*18*/ HPACK_STR("accept-ranges"), HPACK_STR("")}, |
| {/*19*/ HPACK_STR("accept"), HPACK_STR("")}, |
| {/*20*/ HPACK_STR("access-control-allow-origin"),HPACK_STR("")}, |
| {/*21*/ HPACK_STR("age"), HPACK_STR("")}, |
| {/*22*/ HPACK_STR("allow"), HPACK_STR("")}, |
| {/*23*/ HPACK_STR("authorization"), HPACK_STR("")}, |
| {/*24*/ HPACK_STR("cache-control"), HPACK_STR("")}, |
| {/*25*/ HPACK_STR("content-disposition"), HPACK_STR("")}, |
| {/*26*/ HPACK_STR("content-encoding"), HPACK_STR("")}, |
| {/*27*/ HPACK_STR("content-language"), HPACK_STR("")}, |
| {/*28*/ HPACK_STR("content-length"), HPACK_STR("")}, |
| {/*29*/ HPACK_STR("content-location"), HPACK_STR("")}, |
| {/*30*/ HPACK_STR("content-range"), HPACK_STR("")}, |
| {/*31*/ HPACK_STR("content-type"), HPACK_STR("")}, |
| {/*32*/ HPACK_STR("cookie"), HPACK_STR("")}, |
| {/*33*/ HPACK_STR("date"), HPACK_STR("")}, |
| {/*34*/ HPACK_STR("etag"), HPACK_STR("")}, |
| {/*35*/ HPACK_STR("expect"), HPACK_STR("")}, |
| {/*36*/ HPACK_STR("expires"), HPACK_STR("")}, |
| {/*37*/ HPACK_STR("from"), HPACK_STR("")}, |
| {/*38*/ HPACK_STR("host"), HPACK_STR("")}, |
| {/*39*/ HPACK_STR("if-match"), HPACK_STR("")}, |
| {/*40*/ HPACK_STR("if-modified-since"), HPACK_STR("")}, |
| {/*41*/ HPACK_STR("if-none-match"), HPACK_STR("")}, |
| {/*42*/ HPACK_STR("if-range"), HPACK_STR("")}, |
| {/*43*/ HPACK_STR("if-unmodified-since"), HPACK_STR("")}, |
| {/*44*/ HPACK_STR("last-modified"), HPACK_STR("")}, |
| {/*45*/ HPACK_STR("link"), HPACK_STR("")}, |
| {/*46*/ HPACK_STR("location"), HPACK_STR("")}, |
| {/*47*/ HPACK_STR("max-forwards"), HPACK_STR("")}, |
| {/*48*/ HPACK_STR("proxy-authenticate"), HPACK_STR("")}, |
| {/*49*/ HPACK_STR("proxy-authorization"), HPACK_STR("")}, |
| {/*50*/ HPACK_STR("range"), HPACK_STR("")}, |
| {/*51*/ HPACK_STR("referer"), HPACK_STR("")}, |
| {/*52*/ HPACK_STR("refresh"), HPACK_STR("")}, |
| {/*53*/ HPACK_STR("retry-after"), HPACK_STR("")}, |
| {/*54*/ HPACK_STR("server"), HPACK_STR("")}, |
| {/*55*/ HPACK_STR("set-cookie"), HPACK_STR("")}, |
| {/*56*/ HPACK_STR("strict-transport-security"), HPACK_STR("")}, |
| {/*57*/ HPACK_STR("transfer-encoding"), HPACK_STR("")}, |
| {/*58*/ HPACK_STR("user-agent"), HPACK_STR("")}, |
| {/*59*/ HPACK_STR("vary"), HPACK_STR("")}, |
| {/*60*/ HPACK_STR("via"), HPACK_STR("")}, |
| {/*61*/ HPACK_STR("www-authenticate"), HPACK_STR("")} |
| #undef HPACK_STR |
| }; |
| static const apr_uint64_t hpack_static_table_count = |
| (sizeof(hpack_static_table) / sizeof(hpack_static_table[0])); |
| |
| static apr_status_t |
| cleanup_hpack_table(void *data) |
| { |
| #ifdef _DEBUG |
| serf_hpack_table_t *tbl = data; |
| serf_hpack_entry_t *hi, *next; |
| |
| /* This is not really necessary, as we create our own allocator, |
| which lives in the same pool. But it helps tracking down |
| memory leaks in different locations */ |
| for (hi = tbl->lr_first; hi; hi = next) |
| { |
| next = hi->next; |
| |
| hpack_free_entry(hi, tbl->alloc); |
| } |
| tbl->lr_first = tbl->lr_last = NULL; |
| tbl->lr_size = 0; |
| |
| for (hi = tbl->rl_first; hi; hi = next) |
| { |
| next = hi->next; |
| |
| hpack_free_entry(hi, tbl->alloc); |
| } |
| tbl->rl_first = tbl->rl_last = NULL; |
| tbl->rl_size = 0; |
| #endif |
| return APR_SUCCESS; |
| } |
| |
| |
| serf_hpack_table_t * |
| serf__hpack_table_create(int for_http2, |
| apr_size_t default_max_table_size, |
| apr_pool_t *result_pool) |
| { |
| serf_hpack_table_t *tbl = apr_pcalloc(result_pool, sizeof(*tbl)); |
| |
| tbl->pool = result_pool; |
| tbl->alloc = serf_bucket_allocator_create(result_pool, NULL, NULL); |
| |
| /* We register this this after creating the allocator, or we would touch |
| memory that is already freed.*/ |
| apr_pool_cleanup_register(result_pool, tbl, cleanup_hpack_table, |
| apr_pool_cleanup_null); |
| |
| tbl->lr_sys_table_size = tbl->lr_max_table_size = default_max_table_size; |
| tbl->rl_sys_table_size = tbl->rl_max_table_size = default_max_table_size; |
| |
| tbl->lowercase_keys = FALSE; |
| tbl->send_tablesize_update = FALSE; |
| |
| if (for_http2) |
| { |
| /* HTTP2 (aka RFC7540) has some additional rules on how it uses HPACK |
| (aka RFC7541), most notably that all header keys *MUST* be lowercase. |
| |
| Let's keep this thing generic and keep this as a configuration knob. |
| */ |
| tbl->lowercase_keys = TRUE; |
| } |
| |
| return tbl; |
| } |
| |
| static void |
| hpack_shrink_table(serf_hpack_entry_t **first, |
| serf_hpack_entry_t **last, |
| apr_size_t *size, |
| apr_size_t max_size, |
| serf_bucket_alloc_t *allocator) |
| { |
| while (*last && (*size > max_size)) |
| { |
| serf_hpack_entry_t *entry = *last; |
| |
| *last = entry->prev; |
| |
| if (first && (*first == entry)) |
| *first = NULL; |
| |
| if (entry->prev) |
| entry->prev->next = NULL; |
| |
| *size -= HPACK_ENTRY_SIZE(entry); |
| hpack_free_entry(entry, allocator); |
| } |
| } |
| |
| void |
| serf__hpack_table_set_max_table_size(serf_hpack_table_t *hpack_tbl, |
| apr_size_t max_decoder_size, |
| apr_size_t max_encoder_size) |
| { |
| if (max_decoder_size != hpack_tbl->rl_sys_table_size) |
| { |
| hpack_tbl->rl_sys_table_size = max_decoder_size; |
| } |
| |
| if (max_encoder_size != hpack_tbl->lr_max_table_size) |
| { |
| hpack_tbl->lr_sys_table_size = max_encoder_size; |
| |
| if (max_encoder_size > (128 * 1024)) |
| max_encoder_size = (128 * 1024); |
| |
| if (max_encoder_size < hpack_tbl->lr_max_table_size) |
| hpack_tbl->send_tablesize_update = TRUE; |
| |
| hpack_shrink_table(&hpack_tbl->lr_first, |
| &hpack_tbl->lr_last, &hpack_tbl->lr_size, |
| hpack_tbl->lr_max_table_size, hpack_tbl->alloc); |
| } |
| } |
| |
| static apr_status_t |
| hpack_table_size_update(serf_hpack_table_t *hpack_tbl, |
| apr_size_t size) |
| { |
| if (size <= hpack_tbl->rl_sys_table_size) |
| { |
| hpack_tbl->rl_max_table_size = size; |
| |
| hpack_shrink_table(&hpack_tbl->rl_first, |
| &hpack_tbl->rl_last, &hpack_tbl->rl_size, |
| hpack_tbl->rl_max_table_size, hpack_tbl->alloc); |
| } |
| else |
| return SERF_ERROR_HTTP2_COMPRESSION_ERROR; |
| |
| return APR_SUCCESS; |
| } |
| |
| static apr_status_t |
| hpack_table_get(apr_uint32_t v, |
| serf_hpack_table_t *tbl, |
| const char **key, |
| apr_size_t *key_size, |
| const char **value, |
| apr_size_t *value_size) |
| { |
| const serf_hpack_entry_t *entry = NULL; |
| if (v == 0) |
| return SERF_ERROR_HTTP2_COMPRESSION_ERROR; |
| |
| v--; |
| if (v < hpack_static_table_count) |
| entry = &hpack_static_table[v]; |
| else |
| { |
| serf_hpack_entry_t *i; |
| v -= sizeof(hpack_static_table) / sizeof(hpack_static_table[0]); |
| |
| for (i = tbl->rl_first; i; i = i->next) |
| { |
| if (!v) |
| { |
| entry = i; |
| break; |
| } |
| v--; |
| } |
| } |
| |
| if (!entry) |
| return SERF_ERROR_HTTP2_COMPRESSION_ERROR; |
| |
| if (key) |
| *key = entry->key; |
| if (key_size) |
| *key_size = entry->key_len; |
| if (value) |
| *value = entry->value; |
| if (value_size) |
| *value_size = entry->value_len; |
| |
| return APR_SUCCESS; |
| } |
| |
| typedef struct serf_hpack_context_t |
| { |
| serf_hpack_table_t *tbl; |
| |
| serf_hpack_entry_t *first; |
| serf_hpack_entry_t *last; |
| } serf_hpack_context_t; |
| |
| static apr_status_t |
| hpack_copy_from_headers(void *baton, |
| const char *key, |
| const char *value) |
| { |
| serf_bucket_t *hpack = baton; |
| apr_size_t key_sz = strlen(key); |
| |
| /* TODO: Others? */ |
| if ((key_sz == 4 && !strcasecmp(key, "Host")) |
| || (key_sz == 7 && !strcasecmp(key, "Upgrade")) |
| || (key_sz == 10 && !strcasecmp(key, "Keep-Alive")) |
| || (key_sz == 10 && !strcasecmp(key, "Connection")) |
| || (key_sz > 11 && !strncasecmp(key, "Connection-", 11)) |
| || (key_sz == 16 && !strcasecmp(key, "Proxy-Connection")) |
| || (key_sz == 17 && !strcasecmp(key, "Transfer-Encoding"))) |
| { |
| return APR_SUCCESS; |
| } |
| |
| serf__bucket_hpack_setc(hpack, key, value); |
| |
| return APR_SUCCESS; |
| } |
| |
| |
| apr_status_t |
| serf__bucket_hpack_create_from_request(serf_bucket_t **new_hpack_bucket, |
| serf_hpack_table_t *hpack_table, |
| serf_bucket_t *request, |
| const char *scheme, |
| serf_bucket_alloc_t *allocator) |
| { |
| const char *uri, *method, *host; |
| |
| serf_bucket_t *hpack = serf__bucket_hpack_create(hpack_table, allocator); |
| |
| serf_bucket_t *headers = serf_bucket_request_get_headers(request); |
| |
| host = serf_bucket_headers_get(headers, "Host"); |
| |
| serf__bucket_request_read(request, NULL, &uri, &method); |
| |
| serf__bucket_hpack_setc(hpack, ":method", method); |
| serf__bucket_hpack_setc(hpack, ":scheme", scheme); |
| serf__bucket_hpack_setc(hpack, ":authority", host); |
| serf__bucket_hpack_setc(hpack, ":path", uri); |
| |
| serf_bucket_headers_do(headers, hpack_copy_from_headers, hpack); |
| |
| *new_hpack_bucket = hpack; |
| |
| return APR_SUCCESS; |
| } |
| |
| |
| serf_bucket_t * |
| serf__bucket_hpack_create(serf_hpack_table_t *hpack_table, |
| serf_bucket_alloc_t *allocator) |
| { |
| serf_hpack_context_t *ctx = serf_bucket_mem_alloc(allocator, sizeof(*ctx)); |
| |
| ctx->tbl = hpack_table; |
| ctx->first = ctx->last = NULL; |
| |
| return serf_bucket_create(&serf_bucket_type__hpack, allocator, ctx); |
| } |
| |
| void |
| serf__bucket_hpack_setc(serf_bucket_t *hpack_bucket, |
| const char *key, |
| const char *value) |
| { |
| serf__bucket_hpack_setx(hpack_bucket, |
| key, strlen(key), TRUE, |
| value, strlen(value), TRUE, |
| FALSE, FALSE); |
| } |
| |
| void |
| serf__bucket_hpack_setx(serf_bucket_t *bucket, |
| const char *key, |
| apr_size_t key_size, |
| int key_copy, |
| const char *value, |
| apr_size_t value_size, |
| int value_copy, |
| int dont_index, |
| int never_index) |
| { |
| serf_hpack_context_t *ctx = bucket->data; |
| serf_hpack_entry_t *entry; |
| apr_size_t i; |
| |
| for (entry = ctx->first; entry; entry = entry->next) |
| { |
| if (key_size == entry->key_len |
| && !strncasecmp(key, entry->key, key_size)) |
| { |
| break; |
| } |
| } |
| |
| /* TODO: Handle *_copy by keeping some flags */ |
| |
| if (entry && value[0] == ':') |
| { |
| if (entry->free_val) |
| serf_bucket_mem_free(bucket->allocator, (void*)entry->value); |
| |
| entry->value = serf_bstrmemdup(bucket->allocator, value, value_size); |
| entry->value_len = value_size; |
| entry->free_val = true; |
| entry->dont_index = never_index ? 2 : (dont_index ? 1 : 0); |
| |
| return; |
| } |
| else if (entry) |
| { |
| /* We probably want to allow duplicate *and* join behavior? */ |
| } |
| |
| entry = serf_bucket_mem_calloc(bucket->allocator, sizeof(*entry)); |
| |
| if (ctx->tbl && ctx->tbl->lowercase_keys) |
| { |
| /* https://tools.ietf.org/html/rfc7540#section-8.1.2 |
| Just as in HTTP/1.x, header field names are strings of ASCII |
| characters that are compared in a case-insensitive fashion. However, |
| header field names MUST be converted to lowercase prior to their |
| encoding in HTTP/2. A request or response containing uppercase |
| header field names MUST be treated as malformed (Section 8.1.2.6). */ |
| |
| char *ckey = serf_bstrmemdup(bucket->allocator, key, key_size); |
| for (i = 0; i < key_size; i++) |
| { |
| if (ckey[i] >= 'A' && key[i] <= 'Z') |
| ckey[i] += ('a' - 'A'); |
| } |
| entry->key = ckey; |
| entry->free_key = true; |
| } |
| else if (!key_copy) |
| { |
| entry->key = key; |
| entry->free_key = false; |
| } |
| else |
| { |
| entry->key = serf_bstrmemdup(bucket->allocator, key, key_size); |
| entry->free_key = true; |
| } |
| |
| entry->key_len = key_size; |
| if (value_copy) |
| { |
| entry->value = serf_bstrmemdup(bucket->allocator, value, value_size); |
| entry->free_val = true; |
| } |
| else |
| { |
| entry->value = value; |
| entry->free_val = false; |
| } |
| entry->value_len = value_size; |
| entry->dont_index = never_index ? 2 : (dont_index ? 1 : 0); |
| |
| entry->prev = ctx->last; |
| entry->next = NULL; |
| if (ctx->last) |
| { |
| ctx->last->next = entry; |
| ctx->last = entry; |
| } |
| else |
| ctx->first = ctx->last = entry; |
| } |
| |
| const char *serf__bucket_hpack_getc(serf_bucket_t *hpack_bucket, |
| const char *key) |
| { |
| serf_hpack_context_t *ctx = hpack_bucket->data; |
| serf_hpack_entry_t *entry; |
| apr_size_t key_len = strlen(key); |
| |
| for (entry = ctx->first; entry; entry = entry->next) |
| { |
| if (key_len == entry->key_len |
| && !strncasecmp(key, entry->key, key_len)) |
| { |
| return entry->value; |
| } |
| } |
| |
| return NULL; |
| } |
| |
| void serf__bucket_hpack_do(serf_bucket_t *hpack_bucket, |
| serf_bucket_hpack_do_callback_fn_t func, |
| void *baton) |
| { |
| serf_hpack_context_t *ctx = hpack_bucket->data; |
| serf_hpack_entry_t *entry; |
| |
| for (entry = ctx->first; entry; entry = entry->next) |
| { |
| if (func(baton, entry->key, entry->key_len, entry->value, |
| entry->value_len)) |
| { |
| break; |
| } |
| } |
| } |
| |
| static void hpack_int(unsigned char flags, |
| int bits, |
| apr_uint32_t value, |
| char to[6], |
| apr_size_t *used) |
| { |
| unsigned char max_direct; |
| apr_size_t u; |
| |
| flags = flags & ~((1 << bits) - 1); |
| |
| max_direct = (unsigned char)(((apr_uint16_t)1 << bits) - 1); |
| |
| if (value < max_direct) |
| { |
| to[0] = flags | (unsigned char)value; |
| *used = 1; |
| return; |
| } |
| |
| to[0] = flags | max_direct; |
| value -= max_direct; |
| u = 1; |
| |
| while (value >= 0x80) |
| { |
| to[u++] = (value & 0x7F) | 0x80; |
| value >>= 7; |
| } |
| |
| to[u++] = (unsigned char)value; |
| *used = u; |
| } |
| |
| static void |
| serialize_ensure_buffer(serf_bucket_t *bucket, apr_size_t ensure, |
| char **buffer, apr_size_t *offset) |
| { |
| const apr_size_t chunksize = 1024; |
| |
| if (*buffer && ((*offset + ensure) < chunksize)) |
| return; |
| |
| if (*buffer) { |
| serf_bucket_aggregate_append( |
| bucket, |
| serf_bucket_simple_own_create(*buffer, *offset, bucket->allocator)); |
| |
| *buffer = NULL; |
| } |
| *offset = 0; |
| |
| *buffer = serf_bucket_mem_alloc(bucket->allocator, |
| MAX(chunksize, ensure + 64)); |
| *offset = 0; |
| } |
| |
| static apr_status_t |
| serialize(serf_bucket_t *bucket) |
| { |
| serf_hpack_context_t *ctx = bucket->data; |
| serf_bucket_alloc_t *alloc = bucket->allocator; |
| serf_hpack_table_t *tbl = ctx->tbl; |
| |
| serf_hpack_entry_t *entry; |
| serf_hpack_entry_t *next; |
| |
| char *buffer = NULL; |
| apr_size_t offset = 0; |
| |
| /* Put on our aggregate bucket cloak */ |
| serf_bucket_aggregate_become(bucket); |
| |
| /* Is there a tablesize update queued? */ |
| if (tbl && tbl->send_tablesize_update) |
| { |
| apr_size_t len; |
| |
| serialize_ensure_buffer(bucket, 8, &buffer, &offset); |
| |
| hpack_int(0x20, 5, tbl->lr_max_table_size, buffer + offset, &len); |
| offset += len; |
| tbl->send_tablesize_update = FALSE; |
| } |
| |
| for (entry = ctx->first; entry; entry = next) |
| { |
| apr_status_t status; |
| apr_size_t len; |
| apr_uint32_t reuse = 0; |
| const serf_hpack_entry_t *e; |
| bool reuseVal = false; |
| apr_uint32_t i; |
| |
| next = entry->next; |
| |
| serialize_ensure_buffer(bucket, 16, &buffer, &offset); |
| |
| for (i = 0; i < hpack_static_table_count; i++) { |
| e = &hpack_static_table[i]; |
| |
| if (e->key_len == entry->key_len |
| && !memcmp(e->key, entry->key, e->key_len)) |
| { |
| if (e->value_len == entry->value_len |
| && !memcmp(e->value, entry->value, e->value_len)) |
| { |
| reuse = i+1; |
| reuseVal = true; |
| break; |
| } |
| if (!reuse) |
| reuse = i+1; |
| } |
| } |
| if (!reuseVal) { |
| for (e = tbl->lr_first; e; e = e->next) { |
| |
| i++; |
| |
| if (e->key_len == entry->key_len |
| && !memcmp(e->key, entry->key, e->key_len)) |
| { |
| if (e->value_len == entry->value_len |
| && !memcmp(e->value, entry->value, e->value_len)) |
| { |
| reuse = i; |
| reuseVal = true; |
| break; |
| } |
| if (!reuse) |
| reuse = i; |
| } |
| } |
| } |
| |
| if (reuseVal) { |
| /* Nice, we have an exact match of key+value. We can |
| use those, but never index them. */ |
| hpack_int(0x80, 7, reuse, buffer + offset, &len); |
| offset += len; |
| } |
| else if (!entry->dont_index) { |
| /* We reuse the header name, but will add our own value. |
| Or we don't reuse, but do index (value 0) */ |
| hpack_int(0x40, 6, reuse, buffer + offset, &len); |
| offset += len; |
| } |
| else if (entry->dont_index == 2) { |
| /* Never index the value */ |
| hpack_int(0x10, 4, reuse, buffer + offset, &len); |
| offset += len; |
| } |
| else { |
| hpack_int(0x00, 4, reuse, buffer + offset, &len); |
| offset += len; |
| } |
| |
| if (!reuse) { |
| |
| /* To huff or not... */ |
| status = serf__hpack_huffman_encode(entry->key, entry->key_len, |
| 0, NULL, &len); |
| if (!status && len < entry->key_len) |
| { |
| apr_size_t int_len; |
| |
| /* It is more efficient to huffman encode */ |
| serialize_ensure_buffer(bucket, 8 + len, &buffer, &offset); |
| |
| hpack_int(0x80, 7, len, buffer + offset, &int_len); |
| offset += int_len; |
| |
| status = serf__hpack_huffman_encode(entry->key, entry->key_len, |
| len, |
| (void*)(buffer + offset), |
| &len); |
| offset += len; |
| |
| if (status) |
| return status; |
| } |
| else |
| { |
| /* It is more efficient not to encode */ |
| serialize_ensure_buffer(bucket, 8 + entry->key_len, |
| &buffer, &offset); |
| |
| hpack_int(0x00, 7, entry->key_len, buffer + offset, &len); |
| offset += len; |
| |
| memcpy(buffer + offset, entry->key, entry->key_len); |
| offset += entry->key_len; |
| } |
| |
| } |
| if (!reuseVal) { |
| /* To huff or not... */ |
| status = serf__hpack_huffman_encode(entry->value, entry->value_len, |
| 0, NULL, &len); |
| if (!status && len < entry->key_len) |
| { |
| apr_size_t int_len; |
| |
| /* It is more efficient to huffman encode */ |
| serialize_ensure_buffer(bucket, 8 + len, &buffer, &offset); |
| hpack_int(0x80, 7, len, buffer + offset, &int_len); |
| offset += int_len; |
| |
| status = serf__hpack_huffman_encode(entry->value, |
| entry->value_len, |
| len, |
| (void*)(buffer + offset), |
| &len); |
| offset += len; |
| |
| if (status) |
| return status; |
| } |
| else |
| { |
| /* It is more efficient not to encode */ |
| serialize_ensure_buffer(bucket, 8 + entry->value_len, |
| &buffer, &offset); |
| hpack_int(0x00, 7, entry->value_len, buffer + offset, &len); |
| offset += len; |
| |
| memcpy(buffer + offset, entry->value, entry->value_len); |
| offset += entry->value_len; |
| } |
| } |
| |
| /* If we didn't re-use key+value and we are allowed to index the |
| item, let's do that. Note that we already told the other side that |
| we did this, as that side has to keep its table in sync */ |
| if (!reuseVal && !entry->dont_index) { |
| serf_hpack_entry_t *tbl_entry; |
| |
| tbl_entry = serf_bucket_mem_calloc(tbl->alloc, sizeof(*tbl_entry)); |
| |
| tbl_entry->key = serf_bstrmemdup(tbl->alloc, |
| entry->key, entry->key_len); |
| tbl_entry->key_len = entry->key_len; |
| tbl_entry->value = serf_bstrmemdup(tbl->alloc, |
| entry->value, entry->value_len); |
| tbl_entry->value_len = entry->value_len; |
| tbl_entry->free_key = entry->free_val = true; |
| |
| tbl_entry->next = tbl->lr_first; |
| tbl->lr_first = tbl_entry; |
| tbl->lr_size += HPACK_ENTRY_SIZE(tbl_entry); |
| if (tbl_entry->next) |
| tbl_entry->next->prev = tbl_entry; |
| else |
| tbl->lr_last = tbl_entry; |
| |
| if (tbl->lr_size > tbl->rl_max_table_size) { |
| hpack_shrink_table(&tbl->lr_first, |
| &tbl->lr_last, &tbl->lr_size, |
| tbl->lr_max_table_size, tbl->alloc); |
| } |
| } |
| |
| /* And now free the item */ |
| hpack_free_entry(entry, alloc); |
| } |
| ctx->first = ctx->last = NULL; |
| |
| if (buffer) |
| { |
| if (offset) |
| { |
| serf_bucket_aggregate_append( |
| bucket, |
| serf_bucket_simple_own_create(buffer, offset, alloc)); |
| } |
| else |
| serf_bucket_mem_free(alloc, buffer); |
| } |
| |
| serf_bucket_mem_free(alloc, ctx); |
| |
| return APR_SUCCESS; |
| } |
| |
| static apr_status_t |
| serf_hpack_read(serf_bucket_t *bucket, |
| apr_size_t requested, |
| const char **data, |
| apr_size_t *len) |
| { |
| apr_status_t status = serialize(bucket); |
| |
| if (status) |
| return status; |
| |
| return bucket->type->read(bucket, requested, data, len); |
| } |
| |
| static apr_status_t |
| serf_hpack_read_iovec(serf_bucket_t *bucket, |
| apr_size_t requested, |
| int vecs_size, |
| struct iovec *vecs, |
| int *vecs_used) |
| { |
| apr_status_t status = serialize(bucket); |
| |
| if (status) |
| return status; |
| |
| return bucket->type->read_iovec(bucket, requested, vecs_size, vecs, |
| vecs_used); |
| } |
| |
| static apr_status_t |
| serf_hpack_peek(serf_bucket_t *bucket, |
| const char **data, |
| apr_size_t *len) |
| { |
| apr_status_t status = serialize(bucket); |
| |
| if (status) |
| return status; |
| |
| return bucket->type->peek(bucket, data, len); |
| } |
| |
| |
| static apr_uint64_t |
| serf_hpack_get_remaining(serf_bucket_t *bucket) |
| { |
| apr_status_t status = serialize(bucket); |
| |
| if (status) |
| return SERF_LENGTH_UNKNOWN; |
| |
| /* This assumes that the aggregate is a v2 bucket */ |
| return bucket->type->get_remaining(bucket); |
| } |
| |
| |
| static void |
| serf_hpack_destroy_and_data(serf_bucket_t *bucket) |
| { |
| serf_hpack_context_t *ctx = bucket->data; |
| serf_hpack_entry_t *hi; |
| serf_hpack_entry_t *next; |
| |
| for (hi = ctx->first; hi; hi = next) |
| { |
| next = hi->next; |
| |
| hpack_free_entry(hi, bucket->allocator); |
| } |
| |
| serf_default_destroy_and_data(bucket); |
| } |
| |
| |
| const serf_bucket_type_t serf_bucket_type__hpack = { |
| "HPACK", |
| serf_hpack_read, |
| serf_default_readline, |
| serf_hpack_read_iovec, |
| serf_default_read_for_sendfile, |
| serf_buckets_are_v2, |
| serf_hpack_peek, |
| serf_hpack_destroy_and_data, |
| serf_default_read_bucket, |
| serf_hpack_get_remaining, |
| serf_default_ignore_config, |
| }; |
| |
| /* ==================================================================== */ |
| |
| typedef struct serf_hpack_decode_ctx_t |
| { |
| serf_hpack_table_t *tbl; |
| |
| serf_bucket_t *stream; |
| apr_size_t header_allowed; |
| |
| char *buffer; |
| apr_size_t buffer_size; |
| apr_size_t buffer_used; |
| |
| const char *key; /* Allocated in tbl->alloc */ |
| apr_size_t key_size; |
| const char *val; /* Allocated in tbl->alloc */ |
| apr_size_t val_size; |
| char index_item; |
| char key_hm; |
| char val_hm; |
| apr_uint32_t reuse_item; |
| |
| enum |
| { |
| HPACK_DECODE_STATE_INITIAL = 0, |
| HPACK_DECODE_STATE_INDEX, |
| HPACK_DECODE_STATE_KEYINDEX, |
| HPACK_DECODE_STATE_KEY_LEN, |
| HPACK_DECODE_STATE_KEY, |
| HPACK_DECODE_STATE_VALUE_LEN, |
| HPACK_DECODE_STATE_VALUE, |
| HPACK_DECODE_TABLESIZE_UPDATE |
| } state; |
| |
| /* When producing HTTP/1.1 style output */ |
| serf_bucket_t *agg; |
| serf_bucket_t *headers; /* When not NULL added in agg */ |
| serf_config_t *config; |
| |
| bool is_request; |
| bool hit_eof; |
| |
| const char *method; |
| const char *path; |
| const char *authority; |
| } serf_hpack_decode_ctx_t; |
| |
| serf_bucket_t * |
| serf__bucket_hpack_decode_create(serf_bucket_t *stream, |
| apr_size_t max_header_size, |
| serf_hpack_table_t *hpack_table, |
| serf_bucket_alloc_t *alloc) |
| { |
| serf_hpack_decode_ctx_t *ctx = serf_bucket_mem_calloc(alloc, sizeof(*ctx)); |
| |
| ctx->tbl = hpack_table; |
| ctx->stream = stream; |
| ctx->header_allowed = max_header_size; |
| |
| /* The buffer should be large enough to keep a *compressed* key |
| or value and will be resized if necessary. |
| |
| Longer keys are more likely to use compression, so the default |
| should be enough for simple requests. |
| |
| (It is also used for compressed integer values, but there 10 bytes |
| should be enough to store a uint64 with 7 bits/byte) */ |
| ctx->buffer_size = 128; |
| ctx->buffer_used = 0; |
| ctx->buffer = serf_bucket_mem_alloc(alloc, ctx->buffer_size); |
| |
| ctx->agg = serf_bucket_aggregate_create(alloc); |
| ctx->headers = NULL; |
| |
| return serf_bucket_create(&serf_bucket_type__hpack_decode, alloc, ctx); |
| } |
| |
| static void |
| hpack_decode_buffer_ensure(serf_bucket_t *bucket, |
| apr_size_t minsize) |
| { |
| serf_hpack_decode_ctx_t *ctx = bucket->data; |
| char *new_buffer; |
| |
| if (minsize < ctx->buffer_size) |
| return; |
| |
| while (minsize < ctx->buffer_size) |
| { |
| ctx->buffer_size *= 2; |
| } |
| |
| new_buffer = serf_bucket_mem_alloc(bucket->allocator, |
| ctx->buffer_size); |
| |
| /* In general only a small part of the old buffer is used at this point */ |
| memcpy(new_buffer, ctx->buffer, ctx->buffer_used); |
| serf_bucket_mem_free(bucket->allocator, ctx->buffer); |
| ctx->buffer = new_buffer; |
| } |
| |
| static apr_status_t |
| read_hpack_int(apr_uint32_t *v, |
| unsigned char *flags, |
| serf_bucket_t *bucket, |
| int bits) |
| { |
| serf_hpack_decode_ctx_t *ctx = bucket->data; |
| apr_status_t status; |
| apr_uint16_t value_mask; |
| apr_uint64_t vv; |
| |
| if (!ctx->buffer_used) |
| { |
| const char *data; |
| apr_size_t len; |
| |
| status = serf_bucket_read(ctx->stream, 1, &data, &len); |
| |
| if (!status && !len) |
| return SERF_ERROR_EMPTY_READ; |
| else if (SERF_BUCKET_READ_ERROR(status) || len == 0) |
| return status; |
| |
| ctx->buffer[0] = *data; |
| ctx->buffer_used++; |
| } |
| |
| value_mask = (1 << bits) - 1; |
| |
| if (((unsigned char)ctx->buffer[0] & value_mask) != value_mask) |
| { |
| /* Everything fits in the initial byte :-) */ |
| vv = ((unsigned char)ctx->buffer[0] & value_mask); |
| } |
| else |
| { |
| apr_size_t i; |
| |
| /* Here we read the necessary bytes for the integer upto the |
| first byte that doesn't have the 0x80 bit set. |
| |
| We could try to be smart by peeking, getting the size if |
| possible, etc.... but that would optimize for large ints |
| while the value typically fits in 1 or 2 bytes max. |
| |
| My guess is that trying to be smart will be more expensive |
| here. */ |
| do |
| { |
| const char *data; |
| apr_size_t len; |
| |
| /* We already have all the bits we can store */ |
| if ((7 * (ctx->buffer_used - 1) + bits) >= 32) |
| return SERF_ERROR_HTTP2_COMPRESSION_ERROR; |
| |
| status = serf_bucket_read(ctx->stream, 1, &data, &len); |
| if (!status && !len) |
| return SERF_ERROR_EMPTY_READ; |
| else if (SERF_BUCKET_READ_ERROR(status) || len == 0) |
| return status; |
| |
| ctx->buffer[ctx->buffer_used] = *data; |
| ctx->buffer_used++; |
| } while (ctx->buffer[ctx->buffer_used - 1] & 0x80); |
| |
| /* Check if the value could have been stored more efficiently. If it |
| can then this is a compression error. |
| |
| The value where all the bits in the first byte are 1 really |
| needs the next byte as 0, to encode that. */ |
| if (ctx->buffer_used > 2 && ctx->buffer[ctx->buffer_used - 1] == 0) |
| return SERF_ERROR_HTTP2_COMPRESSION_ERROR; |
| |
| vv = value_mask; |
| |
| for (i = 1; i < ctx->buffer_used; i++) |
| vv += (apr_uint64_t)((unsigned char)ctx->buffer[i] & 0x7F) |
| << (7 * (i - 1)); |
| |
| if ((vv & APR_UINT32_MAX) != vv) |
| return SERF_ERROR_HTTP2_COMPRESSION_ERROR; |
| } |
| |
| *v = (apr_uint32_t)vv; |
| |
| if (flags) |
| *flags = (((unsigned char)ctx->buffer[0]) & ~value_mask); |
| |
| ctx->buffer_used = 0; /* Done with buffer */ |
| |
| return APR_SUCCESS; |
| } |
| |
| static void |
| write_request_header(serf_hpack_decode_ctx_t *ctx) |
| { |
| serf_bucket_t *b; |
| serf_bucket_alloc_t *alloc = ctx->agg->allocator; |
| |
| if (ctx->method) |
| b = serf_bucket_simple_own_create(ctx->method, strlen(ctx->method), |
| alloc); |
| else |
| b = SERF_BUCKET_SIMPLE_STRING("GET", alloc); |
| |
| serf_bucket_aggregate_append(ctx->agg, b); |
| |
| b = SERF_BUCKET_SIMPLE_STRING(" ", alloc); |
| serf_bucket_aggregate_append(ctx->agg, b); |
| |
| if (ctx->path) |
| b = serf_bucket_simple_own_create(ctx->path, strlen(ctx->path), |
| alloc); |
| else |
| b = SERF_BUCKET_SIMPLE_STRING("/", alloc); |
| serf_bucket_aggregate_append(ctx->agg, b); |
| |
| b = SERF_BUCKET_SIMPLE_STRING(" HTTP/2.0\r\n", alloc); |
| serf_bucket_aggregate_append(ctx->agg, b); |
| |
| if (ctx->authority) |
| { |
| b = SERF_BUCKET_SIMPLE_STRING("Host: ", alloc); |
| serf_bucket_aggregate_append(ctx->agg, b); |
| |
| b = serf_bucket_simple_own_create(ctx->authority, |
| strlen(ctx->authority), |
| alloc); |
| serf_bucket_aggregate_append(ctx->agg, b); |
| b = SERF_BUCKET_SIMPLE_STRING("\r\n", alloc); |
| serf_bucket_aggregate_append(ctx->agg, b); |
| } |
| /* Now owned by bucket */ |
| ctx->method = ctx->path = ctx->authority = NULL; |
| |
| ctx->headers = serf_bucket_headers_create(ctx->agg->allocator); |
| serf_bucket_aggregate_append(ctx->agg, ctx->headers); |
| } |
| |
| static apr_status_t |
| handle_read_entry_and_clear(serf_hpack_decode_ctx_t *ctx, |
| serf_bucket_alloc_t *alloc) |
| { |
| serf_hpack_table_t *tbl = ctx->tbl; |
| const char *keep_key = NULL; |
| const char *keep_val = NULL; |
| apr_status_t status; |
| char own_key; |
| char own_val; |
| |
| serf__log(LOGLVL_INFO, SERF_LOGCOMP_PROTOCOL, __FILE__, ctx->config, |
| "Parsed from HPACK: %.*s: %.*s\n", |
| (int)ctx->key_size, ctx->key, (int)ctx->val_size, ctx->val); |
| |
| if (!ctx->headers) |
| { |
| serf_bucket_t *b; |
| |
| if (ctx->key_size == 7 && !strcmp(ctx->key, ":status")) |
| { |
| b = SERF_BUCKET_SIMPLE_STRING("HTTP/2.0 ", alloc); |
| serf_bucket_aggregate_append(ctx->agg, b); |
| |
| b = serf_bucket_simple_copy_create(ctx->val, ctx->val_size, alloc); |
| serf_bucket_aggregate_append(ctx->agg, b); |
| |
| b = SERF_BUCKET_SIMPLE_STRING(" <http2>\r\n", alloc); |
| serf_bucket_aggregate_append(ctx->agg, b); |
| |
| ctx->headers = serf_bucket_headers_create(alloc); |
| serf_bucket_aggregate_append(ctx->agg, ctx->headers); |
| } |
| else if (ctx->key_size == 7 && !strcmp(ctx->key, ":method")) |
| { |
| ctx->is_request = true; |
| ctx->method = serf_bstrmemdup(ctx->agg->allocator, |
| ctx->val, ctx->val_size); |
| if (ctx->authority && ctx->method && ctx->path) |
| write_request_header(ctx); |
| } |
| else if (ctx->key_size == 10 && !strcmp(ctx->key, ":authority")) |
| { |
| ctx->is_request = true; |
| ctx->authority = serf_bstrmemdup(ctx->agg->allocator, |
| ctx->val, ctx->val_size); |
| if (ctx->authority && ctx->method && ctx->path) |
| write_request_header(ctx); |
| } |
| else if (ctx->key_size == 5 && !strcmp(ctx->key, ":path")) |
| { |
| ctx->is_request = true; |
| ctx->path = serf_bstrmemdup(ctx->agg->allocator, |
| ctx->val, ctx->val_size); |
| if (ctx->authority && ctx->method && ctx->path) |
| write_request_header(ctx); |
| } |
| else if (ctx->key_size && ctx->key[0] == ':') |
| { |
| /* Ignore all magic headers */ |
| } |
| else |
| { |
| /* Write some header with some status code first */ |
| if (ctx->is_request) |
| write_request_header(ctx); |
| else |
| { |
| b = SERF_BUCKET_SIMPLE_STRING( |
| "HTTP/2.0 505 Missing ':status' header\r\n", |
| alloc); |
| serf_bucket_aggregate_append(ctx->agg, b); |
| |
| ctx->headers = serf_bucket_headers_create(alloc); |
| serf_bucket_aggregate_append(ctx->agg, ctx->headers); |
| } |
| |
| serf_bucket_headers_setc(ctx->headers, ctx->key, ctx->val); |
| } |
| } |
| else if (ctx->key_size && ctx->key[0] != ':') |
| { |
| serf_bucket_headers_setc(ctx->headers, ctx->key, ctx->val); |
| } |
| |
| if (ctx->reuse_item) |
| { |
| status = hpack_table_get(ctx->reuse_item, tbl, |
| &keep_key, NULL, |
| &keep_val, NULL); |
| } |
| |
| own_key = (ctx->key && ctx->key != keep_key); |
| own_val = (ctx->val && ctx->val != keep_val); |
| |
| if (ctx->index_item) |
| { |
| serf_hpack_entry_t *entry = serf_bucket_mem_calloc(tbl->alloc, |
| sizeof(*entry)); |
| |
| entry->key = own_key ? ctx->key : serf_bstrmemdup(tbl->alloc, ctx->key, |
| ctx->key_size); |
| entry->key_len = ctx->key_size; |
| entry->value = own_val ? ctx->val : serf_bstrmemdup(tbl->alloc, |
| ctx->val, |
| ctx->val_size); |
| entry->value_len = ctx->val_size; |
| entry->free_key = entry->free_val = true; |
| entry->next = tbl->rl_first; |
| tbl->rl_first = entry; |
| tbl->rl_size += HPACK_ENTRY_SIZE(entry); |
| if (entry->next) |
| entry->next->prev = entry; |
| else |
| tbl->rl_last = entry; |
| } |
| else |
| { |
| if (own_key) |
| serf_bucket_mem_free(tbl->alloc, (void*)ctx->key); |
| if (own_val) |
| serf_bucket_mem_free(tbl->alloc, (void*)ctx->val); |
| } |
| return APR_SUCCESS; |
| } |
| |
| /* Reads the exact amount of bytes, buffered if necessary. |
| |
| Note: APR_EOF is not returned in case we have everything |
| we need. The callers depend on this behavior |
| */ |
| static apr_status_t hpack_read_bytes(serf_bucket_t *bucket, |
| apr_size_t required, |
| const void **data) |
| { |
| serf_hpack_decode_ctx_t *ctx = bucket->data; |
| apr_status_t status = APR_SUCCESS; |
| apr_size_t len; |
| const char *some_data; |
| |
| /* assert(required < ctx->buffer_used); */ |
| |
| if (required == 0) |
| { |
| *data = ctx->buffer; |
| return APR_SUCCESS; |
| } |
| |
| if (!ctx->buffer_used) |
| { |
| status = serf_bucket_read(ctx->stream, required, &some_data, &len); |
| |
| if (SERF_BUCKET_READ_ERROR(status) || (len == required)) |
| { |
| if (APR_STATUS_IS_EOF(status) && len == required) |
| status = APR_SUCCESS; |
| *data = some_data; |
| return status; |
| } |
| |
| hpack_decode_buffer_ensure(bucket, required); |
| |
| memcpy(ctx->buffer, some_data, len); |
| ctx->buffer_used = len; |
| |
| if (status) |
| return status; |
| |
| /* Fall through: Try to continue reading*/ |
| } |
| else |
| { |
| /* Ensure that the buffer is large enough to hold everything */ |
| hpack_decode_buffer_ensure(bucket, required); |
| } |
| |
| while (ctx->buffer_used < required) |
| { |
| status = serf_bucket_read(ctx->stream, required - ctx->buffer_used, |
| &some_data, &len); |
| |
| if (SERF_BUCKET_READ_ERROR(status)) |
| return status; |
| |
| memcpy(ctx->buffer + ctx->buffer_used, some_data, len); |
| ctx->buffer_used += len; |
| |
| if (status) |
| break; |
| else if (!status && !len) |
| return SERF_ERROR_EMPTY_READ; |
| } |
| |
| if (ctx->buffer_used == required) |
| { |
| *data = ctx->buffer; |
| ctx->buffer_used = 0; /* Done with buffer */ |
| status = APR_SUCCESS; |
| } |
| |
| return status; |
| } |
| |
| static apr_status_t |
| hpack_process(serf_bucket_t *bucket) |
| { |
| serf_hpack_decode_ctx_t *ctx = bucket->data; |
| apr_status_t status = APR_SUCCESS; |
| |
| if (ctx->hit_eof) |
| return APR_SUCCESS; |
| |
| while (status == APR_SUCCESS) |
| { |
| switch (ctx->state) |
| { |
| case HPACK_DECODE_STATE_INITIAL: |
| { |
| unsigned char uc; |
| const char *data; |
| apr_size_t len; |
| |
| status = serf_bucket_read(ctx->stream, 1, &data, &len); |
| if (!status && !len) |
| return SERF_ERROR_EMPTY_READ; |
| else if (SERF_BUCKET_READ_ERROR(status) || len == 0) |
| break; |
| |
| ctx->key_hm = ctx->val_hm = FALSE; |
| ctx->reuse_item = 0; |
| |
| uc = *data; |
| if (uc & 0x80) |
| { |
| /* 6.1. Indexed Header Field Representation |
| https://tools.ietf.org/html/rfc7541#section-6.1 */ |
| |
| ctx->state = HPACK_DECODE_STATE_INDEX; |
| ctx->buffer[0] = *data; |
| ctx->buffer_used = 1; /* Initial state for |
| read_hpack_int() */ |
| ctx->index_item = FALSE; |
| } |
| else if (uc == 0x40 || uc == 0x00 || uc == 0x10) |
| { |
| /* 0x40: Literal Header Field with Incremental Indexing |
| -- New Name |
| https://tools.ietf.org/html/rfc7541#section-6.2.1 |
| 0x00: Literal Header Field without Indexing |
| -- New Name |
| https://tools.ietf.org/html/rfc7541#section-6.2.2 |
| 0x10: Literal Header Field Never Indexed |
| -- New Name |
| https://tools.ietf.org/html/rfc7541#section-6.2.3 */ |
| |
| ctx->state = HPACK_DECODE_STATE_KEY_LEN; |
| ctx->index_item = (uc == 0x40); |
| } |
| else if ((uc & 0x60) == 0x20) |
| { |
| /* 6.3. Dynamic Table Size Update |
| https://tools.ietf.org/html/rfc7541#section-6.3 */ |
| ctx->state = HPACK_DECODE_TABLESIZE_UPDATE; |
| ctx->buffer[0] = *data; |
| ctx->buffer_used = 1; /* Initial state for |
| read_hpack_int() */ |
| } |
| else |
| { |
| /* 6.2.1 Literal Header Field with Incremental Indexing |
| -- Indexed Name |
| https://tools.ietf.org/html/rfc7541#section-6.2.1 |
| 6.2.2: Literal Header Field without Indexing |
| -- Indexed Name |
| https://tools.ietf.org/html/rfc7541#section-6.2.2 |
| 6.2.3. Literal Header Field Never Indexed |
| -- Indexed Name |
| https://tools.ietf.org/html/rfc7541#section-6.2.3 */ |
| |
| ctx->state = HPACK_DECODE_STATE_KEYINDEX; |
| ctx->buffer[0] = *data; |
| ctx->buffer_used = 1; /* Initial state for |
| read_hpack_int() */ |
| ctx->index_item = (uc & 0x40) != 0; |
| } |
| status = APR_SUCCESS; /* Or we exit the loop */ |
| break; |
| } |
| case HPACK_DECODE_STATE_INDEX: |
| { |
| apr_uint32_t v; |
| status = read_hpack_int(&v, NULL, bucket, 7); |
| if (status) |
| break; |
| if (v == 0) |
| return SERF_ERROR_HTTP2_COMPRESSION_ERROR; |
| |
| ctx->reuse_item = v; |
| status = hpack_table_get(v, ctx->tbl, |
| &ctx->key, &ctx->key_size, |
| &ctx->val, &ctx->val_size); |
| if (status) |
| return status; |
| |
| if (ctx->header_allowed <= HPACK_KEY_SIZE(ctx->key_size) |
| + HPACK_KEY_SIZE(ctx->val_size)) |
| { |
| return SERF_ERROR_HTTP2_COMPRESSION_ERROR; |
| } |
| ctx->header_allowed -= HPACK_KEY_SIZE(ctx->key_size) |
| + HPACK_KEY_SIZE(ctx->val_size); |
| |
| status = handle_read_entry_and_clear(ctx, |
| bucket->allocator); |
| if (status) |
| return status; |
| |
| /* Get key and value from table and handle result */ |
| ctx->state = HPACK_DECODE_STATE_INITIAL; |
| break; |
| } |
| case HPACK_DECODE_STATE_KEYINDEX: |
| { |
| apr_uint32_t v; |
| status = read_hpack_int(&v, NULL, bucket, |
| ctx->index_item ? 6 : 4); |
| if (status) |
| continue; |
| |
| ctx->reuse_item = v; |
| status = hpack_table_get(v, ctx->tbl, |
| &ctx->key, &ctx->key_size, |
| NULL, NULL); |
| if (status) |
| return status; |
| |
| /* Get key from table */ |
| ctx->state = HPACK_DECODE_STATE_VALUE_LEN; |
| if (HPACK_KEY_SIZE(ctx->key_size) >= ctx->header_allowed) |
| return SERF_ERROR_HTTP2_COMPRESSION_ERROR; |
| |
| ctx->header_allowed -= HPACK_KEY_SIZE(ctx->key_size); |
| break; |
| } |
| case HPACK_DECODE_STATE_KEY_LEN: |
| { |
| apr_uint32_t v; |
| unsigned char flags; |
| status = read_hpack_int(&v, &flags, bucket, 7); |
| if (status) |
| continue; |
| |
| ctx->key_hm = (flags & 0x80) != 0; |
| |
| /* Just check compressed size first. If the result is |
| smaller the encoder shouldn't have used compression */ |
| if (HPACK_KEY_SIZE(v) >= ctx->header_allowed) |
| return SERF_ERROR_HTTP2_COMPRESSION_ERROR; |
| |
| ctx->key_size = (apr_size_t)v; |
| ctx->state = HPACK_DECODE_STATE_KEY; |
| /* Fall through */ |
| } |
| case HPACK_DECODE_STATE_KEY: |
| { |
| const void *data; |
| |
| status = hpack_read_bytes(bucket, ctx->key_size, &data); |
| if (status) |
| continue; |
| |
| if (ctx->key_hm) |
| { |
| apr_size_t ks; |
| char *key; |
| |
| status = serf__hpack_huffman_decode(data, |
| ctx->key_size, |
| 0, NULL, &ks); |
| |
| if (status) |
| return status; |
| |
| if (HPACK_KEY_SIZE(ks) >= ctx->header_allowed) |
| return SERF_ERROR_HTTP2_COMPRESSION_ERROR; |
| |
| key = serf_bucket_mem_alloc(ctx->tbl->alloc, ks + 1); |
| |
| status = serf__hpack_huffman_decode(data, |
| ctx->key_size, |
| ks + 1, key, |
| &ctx->key_size); |
| if (status) |
| return status; |
| |
| ctx->key = key; |
| } |
| else |
| ctx->key = serf_bstrmemdup(ctx->tbl->alloc, data, |
| ctx->key_size); |
| |
| ctx->state = HPACK_DECODE_STATE_VALUE_LEN; |
| ctx->header_allowed -= HPACK_KEY_SIZE(ctx->key_size); |
| /* Fall through */ |
| } |
| case HPACK_DECODE_STATE_VALUE_LEN: |
| { |
| apr_uint32_t v; |
| unsigned char flags; |
| status = read_hpack_int(&v, &flags, bucket, 7); |
| if (status) |
| continue; |
| |
| ctx->val_hm = (flags & 0x80) != 0; |
| |
| /* Just check compressed size first. If the result is |
| smaller the encoder shouldn't have used compression */ |
| if (HPACK_KEY_SIZE(v) >= ctx->header_allowed) |
| return SERF_ERROR_HTTP2_COMPRESSION_ERROR; |
| |
| ctx->val_size = v; |
| ctx->state = HPACK_DECODE_STATE_VALUE; |
| /* Fall through */ |
| } |
| case HPACK_DECODE_STATE_VALUE: |
| { |
| const void *data; |
| |
| status = hpack_read_bytes(bucket, ctx->val_size, &data); |
| if (status) |
| continue; |
| |
| if (ctx->val_hm) |
| { |
| apr_size_t ks; |
| char *val; |
| |
| status = serf__hpack_huffman_decode(data, |
| ctx->val_size, |
| 0, NULL, &ks); |
| if (status) |
| return status; |
| |
| if (HPACK_KEY_SIZE(ks) >= ctx->header_allowed) |
| return SERF_ERROR_HTTP2_COMPRESSION_ERROR; |
| |
| val = serf_bucket_mem_alloc(ctx->tbl->alloc, ks + 1); |
| |
| status = serf__hpack_huffman_decode(data, |
| ctx->val_size, |
| ks + 1, val, |
| &ctx->val_size); |
| if (status) |
| return status; |
| |
| ctx->val = val; |
| } |
| else |
| ctx->val = serf_bstrmemdup(ctx->tbl->alloc, data, |
| ctx->val_size); |
| |
| ctx->header_allowed -= HPACK_KEY_SIZE(ctx->val_size); |
| |
| status = handle_read_entry_and_clear(ctx, |
| bucket->allocator); |
| if (status) |
| return status; |
| |
| ctx->state = HPACK_DECODE_STATE_INITIAL; |
| break; |
| } |
| case HPACK_DECODE_TABLESIZE_UPDATE: |
| { |
| apr_uint32_t v; |
| |
| status = read_hpack_int(&v, NULL, bucket, 5); |
| if (status) |
| continue; |
| |
| /* Send remote tablesize update to our table */ |
| status = hpack_table_size_update(ctx->tbl, (apr_size_t)v); |
| if (status) |
| return status; |
| |
| ctx->state = HPACK_DECODE_STATE_INITIAL; |
| break; |
| } |
| default: |
| abort(); |
| } |
| } |
| |
| if (APR_STATUS_IS_EOF(status)) |
| { |
| if (ctx->state != HPACK_DECODE_STATE_INITIAL) |
| return SERF_ERROR_HTTP2_COMPRESSION_ERROR; |
| |
| if (!ctx->hit_eof) |
| { |
| serf_hpack_table_t *tbl = ctx->tbl; |
| ctx->hit_eof = TRUE; |
| |
| hpack_shrink_table(&tbl->rl_first, |
| &tbl->rl_last, &tbl->rl_size, |
| tbl->rl_max_table_size, tbl->alloc); |
| } |
| return APR_SUCCESS; |
| } |
| |
| return status; |
| } |
| |
| static apr_status_t |
| serf_hpack_decode_read(serf_bucket_t *bucket, |
| apr_size_t requested, |
| const char **data, |
| apr_size_t *len) |
| { |
| serf_hpack_decode_ctx_t *ctx = bucket->data; |
| apr_status_t status; |
| |
| status = hpack_process(bucket); |
| if (status) |
| { |
| *len = 0; |
| return (status == SERF_ERROR_EMPTY_READ) ? APR_SUCCESS : status; |
| } |
| |
| return serf_bucket_read(ctx->agg, requested, data, len); |
| } |
| |
| static apr_status_t |
| serf_hpack_decode_peek(serf_bucket_t *bucket, |
| const char **data, |
| apr_size_t *len) |
| { |
| serf_hpack_decode_ctx_t *ctx = bucket->data; |
| apr_status_t status; |
| |
| status = hpack_process(bucket); |
| if (status) |
| { |
| *len = 0; |
| return (status == SERF_ERROR_EMPTY_READ) ? APR_SUCCESS : status; |
| } |
| |
| return serf_bucket_peek(ctx->agg, data, len); |
| } |
| |
| static serf_bucket_t * |
| serf_hpack_decode_read_bucket(serf_bucket_t *bucket, |
| const serf_bucket_type_t *type) |
| { |
| serf_hpack_decode_ctx_t *ctx = bucket->data; |
| |
| if (!ctx->hit_eof) |
| return serf_default_read_bucket(bucket, type); |
| else |
| return serf_bucket_read_bucket(ctx->agg, type); |
| } |
| |
| static apr_status_t |
| serf_hpack_decode_set_config(serf_bucket_t *bucket, |
| serf_config_t *config) |
| { |
| serf_hpack_decode_ctx_t *ctx = bucket->data; |
| apr_status_t status; |
| |
| ctx->config = config; |
| |
| status = serf_bucket_set_config(ctx->stream, config); |
| if (status) |
| return status; |
| |
| if (ctx->agg) |
| { |
| status = serf_bucket_set_config(ctx->agg, config); |
| if (status) |
| return status; |
| } |
| return APR_SUCCESS; |
| } |
| |
| static void |
| serf_hpack_decode_destroy(serf_bucket_t *bucket) |
| { |
| serf_hpack_decode_ctx_t *ctx = bucket->data; |
| |
| serf_bucket_destroy(ctx->stream); |
| serf_bucket_destroy(ctx->agg); |
| |
| if (ctx->method) |
| serf_bucket_mem_free(bucket->allocator, (void*)ctx->method); |
| if (ctx->path) |
| serf_bucket_mem_free(bucket->allocator, (void*)ctx->method); |
| if (ctx->authority) |
| serf_bucket_mem_free(bucket->allocator, (void*)ctx->authority); |
| |
| serf_bucket_mem_free(bucket->allocator, ctx->buffer); |
| |
| /* Key and value are handled by table. If we fail reading |
| table can't be used anyway, so the allocator cleanup will |
| handle the leak */ |
| |
| serf_default_destroy_and_data(bucket); |
| } |
| |
| const serf_bucket_type_t serf_bucket_type__hpack_decode = { |
| "HPACK-DECODE", |
| serf_hpack_decode_read, |
| serf_default_readline, |
| serf_default_read_iovec, |
| serf_default_read_for_sendfile, |
| serf_buckets_are_v2, |
| serf_hpack_decode_peek, |
| serf_hpack_decode_destroy, |
| serf_hpack_decode_read_bucket, |
| serf_default_get_remaining, |
| serf_hpack_decode_set_config |
| }; |