blob: 02280a1535e0c40fcb375bf1ffd2a4fcdb584ca0 [file] [log] [blame]
/*
* Copyright (C) Igor Sysoev
* Copyright (C) Nginx, Inc.
* Copyright (C) Google Inc.
*/
extern "C" {
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
}
#include <brotli/enc/encode.h>
typedef struct {
ngx_flag_t enable;
ngx_hash_t types;
ngx_bufs_t bufs;
ngx_int_t quality;
size_t win_bits;
ssize_t min_length;
ngx_array_t *types_keys;
} ngx_http_brotli_conf_t;
typedef struct {
brotli::BrotliCompressor *compressor;
brotli::BrotliParams params;
size_t brotli_ring;
size_t brotli_in;
u_char *brotli_out;
u_char *brotli_last;
size_t bytes_in;
size_t bytes_out;
ngx_chain_t *in;
ngx_chain_t *free;
ngx_chain_t *busy;
ngx_chain_t *out;
ngx_chain_t **last_out;
ngx_buf_t *out_buf;
ngx_int_t bufs;
unsigned done:1;
unsigned sent:1;
unsigned last:1;
unsigned flush:1;
unsigned nomem:1;
} ngx_http_brotli_ctx_t;
static void ngx_http_brotli_filter_params(ngx_http_request_t *r,
ngx_http_brotli_ctx_t *ctx);
static ngx_int_t ngx_http_brotli_filter_add_data(ngx_http_request_t *r,
ngx_http_brotli_ctx_t *ctx);
static ngx_int_t ngx_http_brotli_filter_process(ngx_http_request_t *r,
ngx_http_brotli_ctx_t *ctx);
static ngx_int_t ngx_http_brotli_filter_output(ngx_http_request_t *r,
ngx_http_brotli_ctx_t *ctx);
static ngx_int_t ngx_http_brotli_filter_get_buf(ngx_http_request_t *r,
ngx_http_brotli_ctx_t *ctx);
static void ngx_http_brotli_cleanup(void *data);
static ngx_int_t ngx_http_brotli_ok(ngx_http_request_t *r);
static ngx_int_t ngx_http_brotli_accept_encoding(ngx_str_t *ae);
static ngx_int_t ngx_http_brotli_add_variables(ngx_conf_t *cf);
static ngx_int_t ngx_http_brotli_ratio_variable(ngx_http_request_t *r,
ngx_http_variable_value_t *v, uintptr_t data);
static void *ngx_http_brotli_create_conf(ngx_conf_t *cf);
static char *ngx_http_brotli_merge_conf(ngx_conf_t *cf,
void *parent, void *child);
static ngx_int_t ngx_http_brotli_filter_init(ngx_conf_t *cf);
static char *ngx_http_brotli_window(ngx_conf_t *cf, void *post, void *data);
static ngx_conf_num_bounds_t ngx_http_brotli_comp_level_bounds = {
ngx_conf_check_num_bounds, 0, 11
};
static ngx_conf_post_handler_pt ngx_http_brotli_window_p =
ngx_http_brotli_window;
static ngx_command_t ngx_http_brotli_filter_commands[] = {
{ ngx_string("brotli"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF
|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_brotli_conf_t, enable),
NULL },
{ ngx_string("brotli_buffers"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE2,
ngx_conf_set_bufs_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_brotli_conf_t, bufs),
NULL },
{ ngx_string("brotli_types"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
ngx_http_types_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_brotli_conf_t, types_keys),
&ngx_http_html_default_types[0] },
{ ngx_string("brotli_comp_level"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_brotli_conf_t, quality),
&ngx_http_brotli_comp_level_bounds },
{ ngx_string("brotli_window"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_size_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_brotli_conf_t, win_bits),
&ngx_http_brotli_window_p },
{ ngx_string("brotli_min_length"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_size_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_brotli_conf_t, min_length),
NULL },
ngx_null_command
};
static ngx_http_module_t ngx_http_brotli_filter_module_ctx = {
ngx_http_brotli_add_variables, /* preconfiguration */
ngx_http_brotli_filter_init, /* postconfiguration */
NULL, /* create main configuration */
NULL, /* init main configuration */
NULL, /* create server configuration */
NULL, /* merge server configuration */
ngx_http_brotli_create_conf, /* create location configuration */
ngx_http_brotli_merge_conf /* merge location configuration */
};
ngx_module_t ngx_http_brotli_filter_module = {
NGX_MODULE_V1,
&ngx_http_brotli_filter_module_ctx, /* module context */
ngx_http_brotli_filter_commands, /* module directives */
NGX_HTTP_MODULE, /* module type */
NULL, /* init master */
NULL, /* init module */
NULL, /* init process */
NULL, /* init thread */
NULL, /* exit thread */
NULL, /* exit process */
NULL, /* exit master */
NGX_MODULE_V1_PADDING
};
static ngx_str_t ngx_http_brotli_ratio = ngx_string("brotli_ratio");
static ngx_http_output_header_filter_pt ngx_http_next_header_filter;
static ngx_http_output_body_filter_pt ngx_http_next_body_filter;
static ngx_int_t
ngx_http_brotli_header_filter(ngx_http_request_t *r)
{
ngx_table_elt_t *h;
ngx_http_brotli_ctx_t *ctx;
ngx_http_brotli_conf_t *conf;
conf = reinterpret_cast<ngx_http_brotli_conf_t *>(
ngx_http_get_module_loc_conf(r, ngx_http_brotli_filter_module));
if (!conf->enable
|| (r->headers_out.status != NGX_HTTP_OK
&& r->headers_out.status != NGX_HTTP_FORBIDDEN
&& r->headers_out.status != NGX_HTTP_NOT_FOUND)
|| (r->headers_out.content_encoding
&& r->headers_out.content_encoding->value.len)
|| (r->headers_out.content_length_n != -1
&& r->headers_out.content_length_n < conf->min_length)
|| ngx_http_test_content_type(r, &conf->types) == NULL
|| r->header_only)
{
return ngx_http_next_header_filter(r);
}
r->gzip_vary = 1;
if (ngx_http_brotli_ok(r) != NGX_OK) {
return ngx_http_next_header_filter(r);
}
ctx = reinterpret_cast<ngx_http_brotli_ctx_t *>(
ngx_pcalloc(r->pool, sizeof(ngx_http_brotli_ctx_t)));
if (ctx == NULL) {
return NGX_ERROR;
}
ctx->last_out = &ctx->out;
ngx_http_brotli_filter_params(r, ctx);
ngx_http_set_ctx(r, ctx, ngx_http_brotli_filter_module);
h = reinterpret_cast<ngx_table_elt_t *>(
ngx_list_push(&r->headers_out.headers));
if (h == NULL) {
return NGX_ERROR;
}
h->hash = 1;
ngx_str_set(&h->key, "Content-Encoding");
ngx_str_set(&h->value, "br");
r->headers_out.content_encoding = h;
r->main_filter_need_in_memory = 1;
ngx_http_clear_content_length(r);
ngx_http_clear_accept_ranges(r);
ngx_http_weak_etag(r);
return ngx_http_next_header_filter(r);
}
static ngx_int_t
ngx_http_brotli_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
int rc;
ngx_uint_t flush;
ngx_chain_t *cl;
ngx_pool_cleanup_t *cln;
ngx_http_brotli_ctx_t *ctx;
ctx = reinterpret_cast<ngx_http_brotli_ctx_t *>(
ngx_http_get_module_ctx(r, ngx_http_brotli_filter_module));
if (ctx == NULL || ctx->done || r->header_only) {
return ngx_http_next_body_filter(r, in);
}
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"http brotli filter");
if (ctx->compressor == NULL) {
ctx->compressor = new brotli::BrotliCompressor(ctx->params);
if (ctx->compressor == NULL) {
goto failed;
}
ctx->brotli_ring = ctx->compressor->input_block_size();
ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"brotli compressor: lvl:%d win:%d blk:%uz",
ctx->params.quality, (1 << ctx->params.lgwin),
ctx->brotli_ring);
cln = ngx_pool_cleanup_add(r->pool, 0);
if (cln == NULL) {
goto failed;
}
cln->handler = ngx_http_brotli_cleanup;
cln->data = ctx;
}
if (in) {
if (ngx_chain_add_copy(r->pool, &ctx->in, in) != NGX_OK) {
goto failed;
}
r->connection->buffered |= NGX_HTTP_GZIP_BUFFERED;
}
if (ctx->nomem) {
/* flush busy buffers */
if (ngx_http_next_body_filter(r, NULL) == NGX_ERROR) {
goto failed;
}
cl = NULL;
ngx_chain_update_chains(r->pool, &ctx->free, &ctx->busy, &cl,
(ngx_buf_tag_t) &ngx_http_brotli_filter_module);
ctx->nomem = 0;
flush = 0;
} else {
flush = ctx->busy ? 1 : 0;
}
for ( ;; ) {
/* cycle while we can write to a client */
for ( ;; ) {
rc = ngx_http_brotli_filter_add_data(r, ctx);
if (rc == NGX_DECLINED) {
break;
}
if (rc == NGX_AGAIN) {
continue;
}
rc = ngx_http_brotli_filter_process(r, ctx);
if (rc == NGX_AGAIN) {
continue;
}
if (rc == NGX_ERROR) {
goto failed;
}
rc = ngx_http_brotli_filter_output(r, ctx);
if (rc == NGX_OK) {
break;
}
if (rc == NGX_ERROR) {
goto failed;
}
/* rc == NGX_AGAIN */
}
if (ctx->out == NULL && !flush) {
return ctx->busy ? NGX_AGAIN : NGX_OK;
}
rc = ngx_http_next_body_filter(r, ctx->out);
if (rc == NGX_ERROR) {
goto failed;
}
ngx_chain_update_chains(r->pool, &ctx->free, &ctx->busy, &ctx->out,
(ngx_buf_tag_t) &ngx_http_brotli_filter_module);
ctx->last_out = &ctx->out;
ctx->nomem = 0;
flush = 0;
if (ctx->done) {
ctx->sent = 1;
if (ctx->compressor) {
delete ctx->compressor;
ctx->compressor = NULL;
}
return rc;
}
}
/* unreachable */
failed:
ctx->done = 1;
if (ctx->compressor) {
delete ctx->compressor;
ctx->compressor = NULL;
}
return NGX_ERROR;
}
static void
ngx_http_brotli_filter_params(ngx_http_request_t *r, ngx_http_brotli_ctx_t *ctx)
{
int wbits;
ngx_http_brotli_conf_t *conf;
conf = reinterpret_cast<ngx_http_brotli_conf_t *>(
ngx_http_get_module_loc_conf(r, ngx_http_brotli_filter_module));
wbits = conf->win_bits;
if (r->headers_out.content_length_n > 0) {
while (r->headers_out.content_length_n < (1 << (wbits - 1))
&& wbits > brotli::kMinWindowBits)
{
wbits--;
}
}
ctx->params.quality = conf->quality;
ctx->params.lgwin = wbits;
}
static ngx_int_t
ngx_http_brotli_filter_add_data(ngx_http_request_t *r,
ngx_http_brotli_ctx_t *ctx)
{
size_t size, ring;
ngx_buf_t *b;
if (ctx->brotli_out || ctx->last || ctx->flush) {
return NGX_OK;
}
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"brotli in: %p", ctx->in);
if (ctx->in == NULL) {
return NGX_DECLINED;
}
b = ctx->in->buf;
size = ngx_buf_size(b);
ring = ctx->brotli_ring - ctx->brotli_in;
if (size > ring) {
size = ring;
} else {
if (b->last_buf) {
ctx->last = 1;
} else if (b->flush) {
ctx->flush = 1;
}
}
if (size == ring) {
ctx->flush = 1;
}
if (size > 0) {
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"brotli copy: %p, size:%uz", b, size);
ctx->compressor->CopyInputToRingBuffer(size, b->pos);
ctx->brotli_in += size;
ctx->bytes_in += size;
b->pos += size;
}
if (ngx_buf_size(b) == 0) {
ctx->in = ctx->in->next;
size = 0;
}
if (size == 0 && !ctx->flush && !ctx->last) {
return NGX_AGAIN;
}
return NGX_OK;
}
static ngx_int_t
ngx_http_brotli_filter_process(ngx_http_request_t *r,
ngx_http_brotli_ctx_t *ctx)
{
size_t size;
u_char *out;
if (ctx->brotli_out) {
return NGX_OK;
}
ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"brotli process: size:%uz l:%d f:%d",
ctx->brotli_in, ctx->last, ctx->flush);
out = NULL;
if (!ctx->compressor->WriteBrotliData(ctx->last, ctx->flush, &size, &out)) {
ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0,
"brotli failed: size:%uz l:%d f:%d",
ctx->brotli_in, ctx->last, ctx->flush);
return NGX_ERROR;
}
ctx->brotli_in = 0;
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"brotli data: %p, size:%uz", out, size);
if (size == 0 && !ctx->flush && !ctx->last) {
return NGX_AGAIN;
}
ctx->brotli_out = out;
ctx->brotli_last = out + size;
ctx->bytes_out += size;
return NGX_OK;
}
static ngx_int_t
ngx_http_brotli_filter_output(ngx_http_request_t *r, ngx_http_brotli_ctx_t *ctx)
{
int rc;
size_t size;
ngx_chain_t *cl;
cl = NULL;
while (ctx->brotli_out < ctx->brotli_last) {
rc = ngx_http_brotli_filter_get_buf(r, ctx);
if (rc == NGX_DECLINED) {
return NGX_OK;
}
if (rc == NGX_ERROR) {
return NGX_ERROR;
}
size = ngx_min(ctx->out_buf->end - ctx->out_buf->last,
ctx->brotli_last - ctx->brotli_out);
ngx_memcpy(ctx->out_buf->last, ctx->brotli_out, size);
ctx->out_buf->last += size;
ctx->brotli_out += size;
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"brotli out: %p, size:%uz",
ctx->out_buf, ngx_buf_size(ctx->out_buf));
cl = ngx_alloc_chain_link(r->pool);
if (cl == NULL) {
return NGX_ERROR;
}
cl->buf = ctx->out_buf;
cl->next = NULL;
*ctx->last_out = cl;
ctx->last_out = &cl->next;
}
ctx->brotli_out = NULL;
ctx->brotli_last = NULL;
if (ctx->last || ctx->flush) {
if (ctx->last) {
ctx->done = 1;
if (cl) {
cl->buf->last_buf = 1;
}
} else if (ctx->flush) {
ctx->flush = 0;
if (cl) {
cl->buf->flush = 1;
}
}
r->connection->buffered &= ~NGX_HTTP_GZIP_BUFFERED;
return NGX_OK;
}
return NGX_AGAIN;
}
static ngx_int_t
ngx_http_brotli_filter_get_buf(ngx_http_request_t *r,
ngx_http_brotli_ctx_t *ctx)
{
ngx_http_brotli_conf_t *conf;
if (ctx->free) {
ctx->out_buf = ctx->free->buf;
ctx->free = ctx->free->next;
return NGX_OK;
}
conf = reinterpret_cast<ngx_http_brotli_conf_t *>(
ngx_http_get_module_loc_conf(r, ngx_http_brotli_filter_module));
if (ctx->bufs < conf->bufs.num) {
ctx->out_buf = ngx_create_temp_buf(r->pool, conf->bufs.size);
if (ctx->out_buf == NULL) {
return NGX_ERROR;
}
ctx->out_buf->tag = (ngx_buf_tag_t) &ngx_http_brotli_filter_module;
ctx->out_buf->recycled = 1;
ctx->bufs++;
} else {
ctx->nomem = 1;
return NGX_DECLINED;
}
return NGX_OK;
}
static void
ngx_http_brotli_cleanup(void *data)
{
ngx_http_brotli_ctx_t *ctx;
ctx = reinterpret_cast<ngx_http_brotli_ctx_t *>(data);
if (ctx->compressor) {
delete ctx->compressor;
ctx->compressor = NULL;
}
}
static ngx_int_t
ngx_http_brotli_ok(ngx_http_request_t *r)
{
ngx_table_elt_t *ae;
if (r != r->main) {
return NGX_DECLINED;
}
ae = r->headers_in.accept_encoding;
if (ae == NULL) {
return NGX_DECLINED;
}
if (ae->value.len < sizeof("br") - 1) {
return NGX_DECLINED;
}
if (ngx_memcmp(ae->value.data, "br,", sizeof("br,") - 1) != 0
&& ngx_http_brotli_accept_encoding(&ae->value) != NGX_OK)
{
return NGX_DECLINED;
}
r->gzip_tested = 1;
r->gzip_ok = 0;
return NGX_OK;
}
static ngx_int_t
ngx_http_brotli_accept_encoding(ngx_str_t *ae)
{
u_char *p;
p = ngx_strcasestrn(ae->data, const_cast<char *>("br"), sizeof("br") - 2);
if (p == NULL) {
return NGX_DECLINED;
}
if (p == ae->data || (*(p - 1) == ',' || *(p - 1) == ' ')) {
p += sizeof("br") - 1;
if (p == ae->data + ae->len || *p == ',' || *p == ';' || *p == ' ') {
return NGX_OK;
}
}
return NGX_DECLINED;
}
static ngx_int_t
ngx_http_brotli_add_variables(ngx_conf_t *cf)
{
ngx_http_variable_t *var;
var = ngx_http_add_variable(cf, &ngx_http_brotli_ratio, 0);
if (var == NULL) {
return NGX_ERROR;
}
var->get_handler = ngx_http_brotli_ratio_variable;
return NGX_OK;
}
static ngx_int_t
ngx_http_brotli_ratio_variable(ngx_http_request_t *r,
ngx_http_variable_value_t *v, uintptr_t data)
{
ngx_uint_t ratio_int, ratio_frac;
ngx_http_brotli_ctx_t *ctx;
v->valid = 1;
v->no_cacheable = 0;
v->not_found = 0;
ctx = reinterpret_cast<ngx_http_brotli_ctx_t *>(
ngx_http_get_module_ctx(r, ngx_http_brotli_filter_module));
if (ctx == NULL || ctx->bytes_out == 0 || !ctx->sent) {
v->not_found = 1;
return NGX_OK;
}
v->data = reinterpret_cast<u_char *>(
ngx_pnalloc(r->pool, NGX_INT32_LEN + 3));
if (v->data == NULL) {
return NGX_ERROR;
}
ratio_int = (ngx_uint_t) (ctx->bytes_in / ctx->bytes_out);
ratio_frac = (ngx_uint_t) ((ctx->bytes_in * 100 / ctx->bytes_out) % 100);
if ((ctx->bytes_in * 1000 / ctx->bytes_out) % 10 > 4) {
/* the rounding, e.g., 2.125 to 2.13 */
ratio_frac++;
if (ratio_frac > 99) {
ratio_int++;
ratio_frac = 0;
}
}
v->len = ngx_sprintf(v->data, "%ui.%02ui", ratio_int, ratio_frac) - v->data;
return NGX_OK;
}
static void *
ngx_http_brotli_create_conf(ngx_conf_t *cf)
{
ngx_http_brotli_conf_t *conf;
conf = reinterpret_cast<ngx_http_brotli_conf_t *>(
ngx_pcalloc(cf->pool, sizeof(ngx_http_brotli_conf_t)));
if (conf == NULL) {
return NULL;
}
/*
* set by ngx_pcalloc():
*
* conf->bufs.num = 0;
* conf->types = { NULL };
* conf->types_keys = NULL;
*/
conf->enable = NGX_CONF_UNSET;
conf->quality = NGX_CONF_UNSET;
conf->win_bits = NGX_CONF_UNSET_SIZE;
conf->min_length = NGX_CONF_UNSET;
return conf;
}
static char *
ngx_http_brotli_merge_conf(ngx_conf_t *cf, void *parent, void *child)
{
ngx_http_brotli_conf_t *prev;
ngx_http_brotli_conf_t *conf;
prev = reinterpret_cast<ngx_http_brotli_conf_t *>(parent);
conf = reinterpret_cast<ngx_http_brotli_conf_t *>(child);
ngx_conf_merge_value(conf->enable, prev->enable, 0);
ngx_conf_merge_bufs_value(conf->bufs, prev->bufs,
(128 * 1024) / ngx_pagesize, ngx_pagesize);
ngx_conf_merge_value(conf->quality, prev->quality, 6);
ngx_conf_merge_size_value(conf->win_bits, prev->win_bits, 19);
ngx_conf_merge_value(conf->min_length, prev->min_length, 20);
if (ngx_http_merge_types(cf, &conf->types_keys, &conf->types,
&prev->types_keys, &prev->types,
ngx_http_html_default_types)
!= NGX_OK)
{
return reinterpret_cast<char *>(NGX_CONF_ERROR);
}
return NGX_CONF_OK;
}
static ngx_int_t
ngx_http_brotli_filter_init(ngx_conf_t *cf)
{
ngx_http_next_header_filter = ngx_http_top_header_filter;
ngx_http_top_header_filter = ngx_http_brotli_header_filter;
ngx_http_next_body_filter = ngx_http_top_body_filter;
ngx_http_top_body_filter = ngx_http_brotli_body_filter;
return NGX_OK;
}
static char *
ngx_http_brotli_window(ngx_conf_t *cf, void *post, void *data)
{
size_t *np = reinterpret_cast<size_t *>(data);
size_t wbits, wsize, mbits;
wbits = brotli::kMaxWindowBits;
mbits = brotli::kMinWindowBits;
for (wsize = (1 << wbits); wsize >= (1U << mbits); wsize >>= 1) {
if (wsize == *np) {
*np = wbits;
return NGX_CONF_OK;
}
wbits--;
}
return const_cast<char *>("must be 1k, 2k, 4k, 8k, 16k, 32k, 64k, "
"128k, 256k, 512k, 1m, 2m, 4m, 8m or 16m");
}