summaryrefslogtreecommitdiffhomepage
path: root/src/nxt_http_compression.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/nxt_http_compression.c')
-rw-r--r--src/nxt_http_compression.c586
1 files changed, 586 insertions, 0 deletions
diff --git a/src/nxt_http_compression.c b/src/nxt_http_compression.c
new file mode 100644
index 00000000..dd91d890
--- /dev/null
+++ b/src/nxt_http_compression.c
@@ -0,0 +1,586 @@
+/*
+ *
+ */
+
+#include <nxt_auto_config.h>
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+
+#include <nxt_router.h>
+#include <nxt_http.h>
+#include <nxt_tstr.h>
+#include <nxt_conf.h>
+#include <nxt_http_compression.h>
+
+#define NXT_COMP_LEVEL_UNSET INT8_MIN
+
+typedef enum nxt_http_comp_scheme_e nxt_http_comp_scheme_t;
+typedef struct nxt_http_comp_type_s nxt_http_comp_type_t;
+typedef struct nxt_http_comp_opts_s nxt_http_comp_opts_t;
+typedef struct nxt_http_comp_compressor_s nxt_http_comp_compressor_t;
+typedef struct nxt_http_comp_ctx_s nxt_http_comp_ctx_t;
+
+enum nxt_http_comp_scheme_e {
+ NXT_HTTP_COMP_SCHEME_IDENTITY = 0,
+#if NXT_HAVE_ZLIB
+ NXT_HTTP_COMP_SCHEME_DEFLATE,
+ NXT_HTTP_COMP_SCHEME_GZIP,
+#endif
+#if NXT_HAVE_ZSTD
+ NXT_HTTP_COMP_SCHEME_ZSTD,
+#endif
+#if NXT_HAVE_BROTLI
+ NXT_HTTP_COMP_SCHEME_BROTLI,
+#endif
+
+ /* keep last */
+ NXT_HTTP_COMP_SCHEME_UNKNOWN
+};
+#define NXT_NR_COMPRESSORS NXT_HTTP_COMP_SCHEME_UNKNOWN
+
+struct nxt_http_comp_type_s {
+ nxt_str_t token;
+ nxt_http_comp_scheme_t scheme;
+ int8_t def_compr;
+
+ const nxt_http_comp_operations_t *cops;
+};
+
+struct nxt_http_comp_opts_s {
+ int8_t level;
+ nxt_off_t min_len;
+};
+
+struct nxt_http_comp_compressor_s {
+ const nxt_http_comp_type_t *type;
+ nxt_http_comp_opts_t opts;
+};
+
+struct nxt_http_comp_ctx_s {
+ nxt_uint_t idx;
+
+ nxt_off_t resp_clen;
+ nxt_off_t clen_procd;
+
+ nxt_http_comp_compressor_ctx_t ctx;
+};
+
+static nxt_tstr_t *accept_encoding_query;
+static nxt_http_route_rule_t *mime_types_rule;
+static nxt_http_comp_compressor_t *enabled_compressors;
+static nxt_uint_t nr_enabled_compressors;
+
+static nxt_thread_declare_data(nxt_http_comp_ctx_t, compressor_ctx);
+
+static const nxt_conf_map_t compressors_opts_map[] = {
+ {
+ nxt_string("level"),
+ NXT_CONF_MAP_INT,
+ offsetof(nxt_http_comp_opts_t, level),
+ }, {
+ nxt_string("min_length"),
+ NXT_CONF_MAP_SIZE,
+ offsetof(nxt_http_comp_opts_t, min_len),
+ },
+};
+
+static const nxt_http_comp_type_t compressors[] = {
+ /* Keep this first */
+ {
+ .token = nxt_string("identity"),
+ .scheme = NXT_HTTP_COMP_SCHEME_IDENTITY,
+ },
+#if NXT_HAVE_ZLIB
+ {
+ .token = nxt_string("deflate"),
+ .scheme = NXT_HTTP_COMP_SCHEME_DEFLATE,
+ .def_compr = NXT_HTTP_COMP_ZLIB_DEFAULT_LEVEL,
+ .cops = &nxt_comp_deflate_ops,
+ }, {
+ .token = nxt_string("gzip"),
+ .scheme = NXT_HTTP_COMP_SCHEME_GZIP,
+ .def_compr = NXT_HTTP_COMP_ZLIB_DEFAULT_LEVEL,
+ .cops = &nxt_comp_gzip_ops,
+ },
+#endif
+#if NXT_HAVE_ZSTD
+ {
+ .token = nxt_string("zstd"),
+ .scheme = NXT_HTTP_COMP_SCHEME_ZSTD,
+ .def_compr = NXT_HTTP_COMP_ZSTD_DEFAULT_LEVEL,
+ .cops = &nxt_comp_zstd_ops,
+ },
+#endif
+#if NXT_HAVE_BROTLI
+ {
+ .token = nxt_string("br"),
+ .scheme = NXT_HTTP_COMP_SCHEME_BROTLI,
+ .def_compr = NXT_HTTP_COMP_BROTLI_DEFAULT_LEVEL,
+ .cops = &nxt_comp_brotli_ops,
+ },
+#endif
+};
+
+static void print_compressor(const nxt_http_comp_compressor_t *c)
+{
+ printf("token : %s\n", c->type->token.start);
+ printf("scheme : %d\n", c->type->scheme);
+ printf("level : %d\n", c->opts.level);
+ printf("min_len : %ld\n", c->opts.min_len);
+}
+
+static void print_comp_config(size_t n)
+{
+ for (size_t i = 0; i < n; i++) {
+ nxt_http_comp_compressor_t *compr = enabled_compressors + i;
+
+ print_compressor(compr);
+ printf("\n");
+ }
+}
+
+nxt_int_t
+nxt_http_comp_compress_response(nxt_buf_t *out)
+{
+ nxt_buf_t *b;
+ size_t in_len;
+ size_t buf_len;
+ ssize_t cbytes;
+ uint8_t *buf;
+ bool last = false;
+ nxt_http_comp_compressor_t *compressor;
+
+ printf("%s: \n", __func__);
+
+ b = out;
+
+ if (b->mem.pos == NULL) {
+ return NXT_OK;
+ }
+
+ if (compressor_ctx.idx == NXT_HTTP_COMP_SCHEME_IDENTITY) {
+ printf("%s: NXT_HTTP_COMP_SCHEME_IDENTITY [skipping/identity]\n",
+ __func__);
+ return NXT_OK;
+ }
+
+ compressor = &enabled_compressors[compressor_ctx.idx];
+
+ in_len = b->mem.free - b->mem.pos;
+// last = !b->next || (b->next && b->next->is_last == 1);
+ if ((b->next && b->next->is_last == 1)
+ || compressor_ctx.resp_clen == compressor_ctx.clen_procd
+ + (nxt_off_t)in_len)
+ {
+ last = true;
+ }
+
+ buf_len = compressor->type->cops->compressed_size(&compressor_ctx.ctx,
+ in_len);
+ buf = nxt_malloc(buf_len);
+ cbytes = compressor->type->cops->deflate(&compressor_ctx.ctx, b->mem.pos,
+ in_len, buf, buf_len, last);
+ printf("%s: cbytes = %ld\n", __func__, cbytes);
+ /* XXX What if new buffer is larger than original buffer... ? */
+ if (cbytes != -1) {
+ compressor_ctx.clen_procd += in_len;
+ b->mem.free = nxt_cpymem(b->mem.pos, buf, cbytes);
+ }
+ nxt_free(buf);
+
+ return NXT_OK;
+}
+
+static nxt_uint_t
+nxt_http_comp_compressor_lookup_enabled(const nxt_str_t *token)
+{
+ if (token->start[0] == '*') {
+ return NXT_HTTP_COMP_SCHEME_IDENTITY;
+ }
+
+ for (size_t i = 0, n = nr_enabled_compressors; i < n; i++) {
+ if (nxt_strstr_eq(token, &enabled_compressors[i].type->token)) {
+ return i;
+ }
+ }
+
+ return NXT_HTTP_COMP_SCHEME_UNKNOWN;
+}
+
+/*
+ * We need to parse the 'Accept-Encoding` header as described by
+ * <https://www.rfc-editor.org/rfc/rfc9110.html#field.accept-encoding>
+ * which can take forms such as
+ *
+ * Accept-Encoding: compress, gzip
+ * Accept-Encoding:
+ * Accept-Encoding: *
+ * Accept-Encoding: compress;q=0.5, gzip;q=1.0
+ * Accept-Encoding: gzip;q=1.0, identity;q=0.5, *;q=0
+ *
+ * '*:q=0' means if the content being served has no 'Content-Coding'
+ * matching an 'Accept-Encoding' entry then don't send any response.
+ *
+ * 'indentity;q=0' seems to basically mean the same thing...
+ */
+static nxt_int_t
+nxt_http_comp_select_compressor(const nxt_str_t *token)
+{
+ bool identity_allowed = true;
+ char *str;
+ char *saveptr;
+ double weight = 0.0;
+ nxt_int_t idx = 0;
+
+ str = strndup((char *)token->start, token->length);
+
+ for ( ; ; str = NULL) {
+ char *tkn, *qptr;
+ double qval = -1.0;
+ nxt_uint_t ecidx;
+ nxt_str_t enc;
+ nxt_http_comp_scheme_t scheme;
+
+ tkn = strtok_r(str, ", ", &saveptr);
+ if (tkn == NULL) {
+ break;
+ }
+
+ qptr = strstr(tkn, ";q=");
+ if (qptr != NULL) {
+ qval = atof(qptr + 3);
+ }
+
+ enc.start = (u_char *)tkn;
+ enc.length = qptr != NULL ? (size_t)(qptr - tkn) : strlen(tkn);
+
+ ecidx = nxt_http_comp_compressor_lookup_enabled(&enc);
+ if (ecidx == NXT_HTTP_COMP_SCHEME_UNKNOWN) {
+ continue;
+ }
+
+ scheme = enabled_compressors[ecidx].type->scheme;
+
+ printf("%s: %.*s [%f] [%d:%d]\n", __func__, (int)enc.length, enc.start,
+ qval, ecidx, scheme);
+
+ if (qval == 0.0 && scheme == NXT_HTTP_COMP_SCHEME_IDENTITY) {
+ identity_allowed = false;
+ }
+
+ if (qval != -1.0 && (qval == 0.0 || qval < weight)) {
+ continue;
+ }
+
+ idx = ecidx;
+ weight = qval;
+ }
+
+ free(str);
+
+ printf("%s: Selected compressor : %s\n", __func__,
+ enabled_compressors[idx].type->token.start);
+
+ printf("%s: idx [%u], identity_allowed [%s]\n", __func__, idx,
+ identity_allowed ? "true" : "false");
+
+ if (idx == NXT_HTTP_COMP_SCHEME_IDENTITY && !identity_allowed) {
+ return -1;
+ }
+
+ return idx;
+}
+
+static nxt_int_t
+nxt_http_comp_set_header(nxt_http_request_t *r, nxt_uint_t index)
+{
+ const nxt_str_t *token;
+ nxt_http_field_t *f;
+
+ static const nxt_str_t content_encoding_str =
+ nxt_string("Content-Encoding");
+
+ f = nxt_list_add(r->resp.fields);
+ if (nxt_slow_path(f == NULL)) {
+ return NXT_ERROR;
+ }
+
+ r->resp.content_length = NULL;
+ r->resp.content_length_n = -1;
+
+ f->hash = 0;
+ f->skip = 0;
+ f->hopbyhop = 0;
+
+ token = &enabled_compressors[index].type->token;
+
+ f->name = content_encoding_str.start;
+ f->name_length = content_encoding_str.length;
+ f->value = token->start;
+ f->value_length = token->length;
+
+ return NXT_OK;
+}
+
+static bool
+nxt_http_comp_is_resp_content_encoded(const nxt_http_request_t *r)
+{
+ nxt_http_field_t *f;
+
+ printf("%s: \n", __func__);
+
+ nxt_list_each(f, r->resp.fields) {
+ printf("%s: %s: %s\n", __func__, f->name, f->value);
+ if (nxt_strcasecmp(f->name, (const u_char *)"Content-Encoding") == 0) {
+ return true;
+ }
+ } nxt_list_loop;
+
+ return false;
+}
+
+nxt_int_t
+nxt_http_comp_check_compression(nxt_task_t *task, nxt_http_request_t *r)
+{
+ int8_t level;
+ nxt_str_t mime_type = { };
+ nxt_int_t ret, idx;
+ nxt_off_t min_len;
+ nxt_str_t accept_encoding;
+ nxt_router_conf_t *rtcf;
+ nxt_http_comp_compressor_t *compressor;
+
+ printf("%s: \n", __func__);
+
+ compressor_ctx = (nxt_http_comp_ctx_t){ .resp_clen = -1 };
+
+ if (r->resp.content_length_n == 0) {
+ return NXT_OK;
+ }
+
+ if (r->resp.mime_type != NULL) {
+ mime_type = *r->resp.mime_type;
+ } else if (r->resp.content_type != NULL) {
+ mime_type.start = r->resp.content_type->value;
+ mime_type.length = r->resp.content_type->value_length;
+ }
+
+ if (mime_type.start == NULL) {
+ return NXT_OK;
+ }
+
+ printf("%s: Response Content-Type [%.*s]\n", __func__,
+ (int)mime_type.length, mime_type.start);
+
+ if (mime_types_rule != NULL) {
+ ret = nxt_http_route_test_rule(r, mime_types_rule,
+ mime_type.start,
+ mime_type.length);
+ printf("%s: mime_type : %d (%.*s)\n", __func__, ret,
+ (int)mime_type.length, mime_type.start);
+
+ if (ret == 0) {
+ return NXT_OK;
+ }
+ }
+
+ rtcf = r->conf->socket_conf->router_conf;
+
+ if (nxt_http_comp_is_resp_content_encoded(r)) {
+ return NXT_OK;
+ }
+
+ /* XXX Should checking the Accept-Encoding header come first? */
+ ret = nxt_tstr_query_init(&r->tstr_query, rtcf->tstr_state, &r->tstr_cache,
+ r, r->mem_pool);
+ if (nxt_slow_path(ret == NXT_ERROR)) {
+ return NXT_ERROR;
+ }
+
+ nxt_tstr_query(task, r->tstr_query, accept_encoding_query,
+ &accept_encoding);
+ if (nxt_slow_path(nxt_tstr_query_failed(r->tstr_query))) {
+ return NXT_ERROR;
+ }
+
+ printf("%s: Accept-Encoding: %s\n", __func__, accept_encoding.start);
+
+ idx = nxt_http_comp_select_compressor(&accept_encoding);
+ if (idx == -1) {
+ /*
+ * XXX Needs to trigger the right HTTP error status code
+ * for unsuppported media or something...
+ */
+ return NXT_ERROR;
+ }
+
+ if (idx == NXT_HTTP_COMP_SCHEME_IDENTITY) {
+ return NXT_OK;
+ }
+
+ compressor = &enabled_compressors[idx];
+
+ if (r->resp.content_length_n > -1) {
+ compressor_ctx.resp_clen = r->resp.content_length_n;
+ } else if (r->resp.content_length != NULL) {
+ compressor_ctx.resp_clen =
+ strtol((char *)r->resp.content_length->value, NULL, 10);
+ }
+
+ min_len = compressor->opts.min_len;
+
+ if (compressor_ctx.resp_clen > -1 && compressor_ctx.resp_clen < min_len) {
+ printf("%s: %ld < %ld [skipping/clen]\n", __func__,
+ compressor_ctx.resp_clen , min_len);
+ return NXT_OK;
+ }
+
+ nxt_http_comp_set_header(r, idx);
+
+ compressor_ctx.idx = idx;
+
+ level = enabled_compressors[idx].opts.level;
+ compressor_ctx.ctx.level = level == NXT_COMP_LEVEL_UNSET ?
+ enabled_compressors[idx].type->def_compr : level;
+
+ compressor->type->cops->init(&compressor_ctx.ctx);
+
+ return NXT_OK;
+}
+
+static nxt_uint_t
+nxt_http_comp_compressor_token2idx(const nxt_str_t *token)
+{
+ for (nxt_uint_t i = 0, n = nxt_nitems(compressors); i < n; i++) {
+ if (nxt_strstr_eq(token, &compressors[i].token)) {
+ return i;
+ }
+ }
+
+ return NXT_HTTP_COMP_SCHEME_UNKNOWN;
+}
+
+bool
+nxt_http_comp_compressor_is_valid(const nxt_str_t *token)
+{
+ nxt_uint_t idx;
+
+ idx = nxt_http_comp_compressor_token2idx(token);
+ if (idx != NXT_HTTP_COMP_SCHEME_UNKNOWN) {
+ return true;
+ }
+
+ return false;
+}
+
+static nxt_int_t
+nxt_http_comp_set_compressor(nxt_router_conf_t *rtcf,
+ const nxt_conf_value_t *comp, nxt_uint_t index)
+{
+ nxt_int_t ret;
+ nxt_str_t token;
+ nxt_uint_t cidx;
+ nxt_conf_value_t *obj;
+
+ printf("%s: \n", __func__);
+
+ static nxt_str_t token_str = nxt_string("encoding");
+
+ obj = nxt_conf_get_object_member(comp, &token_str, NULL);
+ if (obj == NULL) {
+ return NXT_ERROR;
+ }
+
+ nxt_conf_get_string(obj, &token);
+ cidx = nxt_http_comp_compressor_token2idx(&token);
+
+ enabled_compressors[index].type = &compressors[cidx];
+ enabled_compressors[index].opts.level = NXT_COMP_LEVEL_UNSET;
+ enabled_compressors[index].opts.min_len = -1;
+ printf("%s: %s\n", __func__, enabled_compressors[index].type->token.start);
+
+ ret = nxt_conf_map_object(rtcf->mem_pool, comp, compressors_opts_map,
+ nxt_nitems(compressors_opts_map),
+ &enabled_compressors[index].opts);
+ if (nxt_slow_path(ret == NXT_ERROR)) {
+ return NXT_ERROR;
+ }
+
+ return NXT_OK;
+}
+
+nxt_int_t
+nxt_http_comp_compression_init(nxt_task_t *task, nxt_router_conf_t *rtcf,
+ const nxt_conf_value_t *comp_conf)
+{
+ nxt_int_t ret;
+ nxt_uint_t n = 1; /* 'identity' */
+ nxt_conf_value_t *comps, *mimes;
+
+ static const nxt_str_t accept_enc_str =
+ nxt_string("$header_accept_encoding");
+ static const nxt_str_t comps_str = nxt_string("compressors");
+ static const nxt_str_t mimes_str = nxt_string("types");
+
+ printf("%s: \n", __func__);
+
+ mimes = nxt_conf_get_object_member(comp_conf, &mimes_str, NULL);
+ if (mimes != NULL) {
+ mime_types_rule = nxt_http_route_types_rule_create(task,
+ rtcf->mem_pool,
+ mimes);
+ if (nxt_slow_path(mime_types_rule == NULL)) {
+ return NXT_ERROR;
+ }
+ }
+
+ accept_encoding_query = nxt_tstr_compile(rtcf->tstr_state, &accept_enc_str,
+ NXT_TSTR_STRZ);
+ if (nxt_slow_path(accept_encoding_query == NULL)) {
+ return NXT_ERROR;
+ }
+
+ comps = nxt_conf_get_object_member(comp_conf, &comps_str, NULL);
+ if (nxt_slow_path(comps == NULL)) {
+ return NXT_ERROR;
+ }
+
+ if (nxt_conf_type(comps) == NXT_CONF_OBJECT) {
+ n++;
+ } else {
+ n += nxt_conf_object_members_count(comps);
+ }
+ nr_enabled_compressors = n;
+
+ enabled_compressors = nxt_mp_zalloc(rtcf->mem_pool,
+ sizeof(nxt_http_comp_compressor_t) * n);
+
+ enabled_compressors[0] =
+ (nxt_http_comp_compressor_t){ .type = &compressors[0],
+ .opts.level = NXT_COMP_LEVEL_UNSET,
+ .opts.min_len = -1 };
+
+ if (nxt_conf_type(comps) == NXT_CONF_OBJECT) {
+ /* XXX Remove me... */
+ print_comp_config(nr_enabled_compressors);
+ return nxt_http_comp_set_compressor(rtcf, comps, 1);
+ }
+
+ for (nxt_uint_t i = 1; i < n; i++) {
+ nxt_conf_value_t *obj;
+
+ obj = nxt_conf_get_array_element(comps, i - 1);
+ ret = nxt_http_comp_set_compressor(rtcf, obj, i);
+ if (ret == NXT_ERROR) {
+ return NXT_ERROR;
+ }
+ }
+
+ nr_enabled_compressors = n;
+ /* XXX Remove me... */
+ print_comp_config(nr_enabled_compressors);
+
+ return NXT_OK;
+}