
/*
 * Copyright (C) Roman Arutyunyan
 * Copyright (C) Dmitry Volyntsev
 * Copyright (C) NGINX, Inc.
 */


#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_stream.h>
#include "ngx_js.h"


typedef struct {
    njs_vm_t              *vm;
    ngx_str_t              include;
    u_char                *file;
    ngx_uint_t             line;
    ngx_array_t           *imports;
    ngx_array_t           *paths;
} ngx_stream_js_main_conf_t;


typedef struct {
    ngx_str_t              name;
    ngx_str_t              path;
    u_char                *file;
    ngx_uint_t             line;
} ngx_stream_js_import_t;


typedef struct {
    ngx_str_t              access;
    ngx_str_t              preread;
    ngx_str_t              filter;
#if (NGX_SSL)
    ngx_ssl_t             *ssl;
    ngx_str_t              ssl_ciphers;
    ngx_uint_t             ssl_protocols;
    ngx_int_t              ssl_verify_depth;
    ngx_str_t              ssl_trusted_certificate;
#endif
} ngx_stream_js_srv_conf_t;


typedef struct {
    njs_vm_event_t          ev;
    ngx_uint_t              data_type;
} ngx_stream_js_ev_t;


typedef struct {
    njs_vm_t               *vm;
    njs_opaque_value_t      retval;
    njs_opaque_value_t      args[3];
    ngx_buf_t              *buf;
    ngx_chain_t           **last_out;
    ngx_chain_t            *free;
    ngx_chain_t            *upstream_busy;
    ngx_chain_t            *downstream_busy;
    ngx_int_t               status;
#define NGX_JS_EVENT_UPLOAD   0
#define NGX_JS_EVENT_DOWNLOAD 1
#define NGX_JS_EVENT_MAX      2
    ngx_stream_js_ev_t      events[2];
    unsigned                from_upstream:1;
    unsigned                filter:1;
    unsigned                in_progress:1;
} ngx_stream_js_ctx_t;


typedef struct {
    ngx_stream_session_t  *session;
    njs_vm_event_t         vm_event;
    void                  *unused;
    ngx_int_t              ident;
} ngx_stream_js_event_t;


static ngx_int_t ngx_stream_js_access_handler(ngx_stream_session_t *s);
static ngx_int_t ngx_stream_js_preread_handler(ngx_stream_session_t *s);
static ngx_int_t ngx_stream_js_phase_handler(ngx_stream_session_t *s,
    ngx_str_t *name);
static ngx_int_t ngx_stream_js_body_filter(ngx_stream_session_t *s,
    ngx_chain_t *in, ngx_uint_t from_upstream);
static ngx_int_t ngx_stream_js_variable_set(ngx_stream_session_t *s,
    ngx_stream_variable_value_t *v, uintptr_t data);
static ngx_int_t ngx_stream_js_variable_var(ngx_stream_session_t *s,
    ngx_stream_variable_value_t *v, uintptr_t data);
static ngx_int_t ngx_stream_js_init_vm(ngx_stream_session_t *s);
static void ngx_stream_js_drop_events(ngx_stream_js_ctx_t *ctx);
static void ngx_stream_js_cleanup(void *data);
static void ngx_stream_js_cleanup_vm(void *data);
static njs_int_t ngx_stream_js_run_event(ngx_stream_session_t *s,
    ngx_stream_js_ctx_t *ctx, ngx_stream_js_ev_t *event);
static njs_vm_event_t *ngx_stream_js_event(ngx_stream_session_t *s,
    njs_str_t *event);

static njs_int_t ngx_stream_js_ext_get_remote_address(njs_vm_t *vm,
    njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval,
    njs_value_t *retval);

static njs_int_t ngx_stream_js_ext_done(njs_vm_t *vm, njs_value_t *args,
     njs_uint_t nargs, njs_index_t unused);

static njs_int_t ngx_stream_js_ext_on(njs_vm_t *vm, njs_value_t *args,
     njs_uint_t nargs, njs_index_t unused);
static njs_int_t ngx_stream_js_ext_off(njs_vm_t *vm, njs_value_t *args,
     njs_uint_t nargs, njs_index_t unused);
static njs_int_t ngx_stream_js_ext_send(njs_vm_t *vm, njs_value_t *args,
     njs_uint_t nargs, njs_index_t unused);
static njs_int_t ngx_stream_js_ext_set_return_value(njs_vm_t *vm,
    njs_value_t *args, njs_uint_t nargs, njs_index_t unused);

static njs_int_t ngx_stream_js_ext_variables(njs_vm_t *vm,
    njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval,
    njs_value_t *retval);

static njs_host_event_t ngx_stream_js_set_timer(njs_external_ptr_t external,
    uint64_t delay, njs_vm_event_t vm_event);
static void ngx_stream_js_clear_timer(njs_external_ptr_t external,
    njs_host_event_t event);
static void ngx_stream_js_timer_handler(ngx_event_t *ev);
static ngx_pool_t *ngx_stream_js_pool(njs_vm_t *vm, ngx_stream_session_t *s);
static ngx_resolver_t *ngx_stream_js_resolver(njs_vm_t *vm,
    ngx_stream_session_t *s);
static ngx_msec_t ngx_stream_js_resolver_timeout(njs_vm_t *vm,
    ngx_stream_session_t *s);
static void ngx_stream_js_handle_event(ngx_stream_session_t *s,
    njs_vm_event_t vm_event, njs_value_t *args, njs_uint_t nargs);

static char *ngx_stream_js_include(ngx_conf_t *cf, ngx_command_t *cmd,
    void *conf);
static char *ngx_stream_js_import(ngx_conf_t *cf, ngx_command_t *cmd,
    void *conf);
static char *ngx_stream_js_set(ngx_conf_t *cf, ngx_command_t *cmd,
    void *conf);
static char *ngx_stream_js_var(ngx_conf_t *cf, ngx_command_t *cmd,
    void *conf);
static void *ngx_stream_js_create_main_conf(ngx_conf_t *cf);
static char *ngx_stream_js_init_main_conf(ngx_conf_t *cf, void *conf);
static void *ngx_stream_js_create_srv_conf(ngx_conf_t *cf);
static char *ngx_stream_js_merge_srv_conf(ngx_conf_t *cf, void *parent,
    void *child);
static ngx_int_t ngx_stream_js_init(ngx_conf_t *cf);

#if (NGX_SSL)
static char * ngx_stream_js_set_ssl(ngx_conf_t *cf,
    ngx_stream_js_srv_conf_t *jscf);
#endif
static ngx_ssl_t *ngx_stream_js_ssl(njs_vm_t *vm, ngx_stream_session_t *s);

#if (NGX_HTTP_SSL)

static ngx_conf_bitmask_t  ngx_stream_js_ssl_protocols[] = {
    { ngx_string("TLSv1"), NGX_SSL_TLSv1 },
    { ngx_string("TLSv1.1"), NGX_SSL_TLSv1_1 },
    { ngx_string("TLSv1.2"), NGX_SSL_TLSv1_2 },
    { ngx_string("TLSv1.3"), NGX_SSL_TLSv1_3 },
    { ngx_null_string, 0 }
};

#endif

static ngx_command_t  ngx_stream_js_commands[] = {

    { ngx_string("js_include"),
      NGX_STREAM_MAIN_CONF|NGX_CONF_TAKE1,
      ngx_stream_js_include,
      NGX_STREAM_MAIN_CONF_OFFSET,
      offsetof(ngx_stream_js_main_conf_t, include),
      NULL },

    { ngx_string("js_import"),
      NGX_STREAM_MAIN_CONF|NGX_CONF_TAKE13,
      ngx_stream_js_import,
      NGX_STREAM_MAIN_CONF_OFFSET,
      0,
      NULL },

    { ngx_string("js_path"),
      NGX_STREAM_MAIN_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_str_array_slot,
      NGX_STREAM_MAIN_CONF_OFFSET,
      offsetof(ngx_stream_js_main_conf_t, paths),
      NULL },

    { ngx_string("js_set"),
      NGX_STREAM_MAIN_CONF|NGX_CONF_TAKE2,
      ngx_stream_js_set,
      0,
      0,
      NULL },

    { ngx_string("js_var"),
      NGX_STREAM_MAIN_CONF|NGX_CONF_TAKE12,
      ngx_stream_js_var,
      0,
      0,
      NULL },

    { ngx_string("js_access"),
      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_str_slot,
      NGX_STREAM_SRV_CONF_OFFSET,
      offsetof(ngx_stream_js_srv_conf_t, access),
      NULL },

    { ngx_string("js_preread"),
      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_str_slot,
      NGX_STREAM_SRV_CONF_OFFSET,
      offsetof(ngx_stream_js_srv_conf_t, preread),
      NULL },

    { ngx_string("js_filter"),
      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_str_slot,
      NGX_STREAM_SRV_CONF_OFFSET,
      offsetof(ngx_stream_js_srv_conf_t, filter),
      NULL },

#if (NGX_SSL)

    { ngx_string("js_fetch_ciphers"),
      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_str_slot,
      NGX_STREAM_SRV_CONF_OFFSET,
      offsetof(ngx_stream_js_srv_conf_t, ssl_ciphers),
      NULL },

    { ngx_string("js_fetch_protocols"),
      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_1MORE,
      ngx_conf_set_bitmask_slot,
      NGX_STREAM_SRV_CONF_OFFSET,
      offsetof(ngx_stream_js_srv_conf_t, ssl_protocols),
      &ngx_stream_js_ssl_protocols },

    { ngx_string("js_fetch_verify_depth"),
      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_num_slot,
      NGX_STREAM_SRV_CONF_OFFSET,
      offsetof(ngx_stream_js_srv_conf_t, ssl_verify_depth),
      NULL },

    { ngx_string("js_fetch_trusted_certificate"),
      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_str_slot,
      NGX_STREAM_SRV_CONF_OFFSET,
      offsetof(ngx_stream_js_srv_conf_t, ssl_trusted_certificate),
      NULL },

#endif

      ngx_null_command
};


static ngx_stream_module_t  ngx_stream_js_module_ctx = {
    NULL,                           /* preconfiguration */
    ngx_stream_js_init,             /* postconfiguration */

    ngx_stream_js_create_main_conf, /* create main configuration */
    ngx_stream_js_init_main_conf,   /* init main configuration */

    ngx_stream_js_create_srv_conf,  /* create server configuration */
    ngx_stream_js_merge_srv_conf,   /* merge server configuration */
};


ngx_module_t  ngx_stream_js_module = {
    NGX_MODULE_V1,
    &ngx_stream_js_module_ctx,      /* module context */
    ngx_stream_js_commands,         /* module directives */
    NGX_STREAM_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 njs_external_t  ngx_stream_js_ext_session[] = {

    {
        .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
        .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
        .u.property = {
            .value = "Stream Session",
        }
    },

    {
        .flags = NJS_EXTERN_PROPERTY,
        .name.string = njs_str("status"),
        .enumerable = 1,
        .u.property = {
            .handler = ngx_js_ext_uint,
            .magic32 = offsetof(ngx_stream_session_t, status),
        }
    },

    {
        .flags = NJS_EXTERN_PROPERTY,
        .name.string = njs_str("remoteAddress"),
        .enumerable = 1,
        .u.property = {
            .handler = ngx_stream_js_ext_get_remote_address,
        }
    },

    {
        .flags = NJS_EXTERN_OBJECT,
        .name.string = njs_str("variables"),
        .u.object = {
            .writable = 1,
            .prop_handler = ngx_stream_js_ext_variables,
            .magic32 = NGX_JS_STRING,
        }
    },

    {
        .flags = NJS_EXTERN_OBJECT,
        .name.string = njs_str("rawVariables"),
        .u.object = {
            .writable = 1,
            .prop_handler = ngx_stream_js_ext_variables,
            .magic32 = NGX_JS_BUFFER,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("allow"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_stream_js_ext_done,
            .magic8 = NGX_OK,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("deny"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_stream_js_ext_done,
            .magic8 = -NGX_DONE,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("decline"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_stream_js_ext_done,
            .magic8 = -NGX_DECLINED,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("done"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_stream_js_ext_done,
            .magic8 = NGX_OK,

        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("log"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_js_ext_log,
            .magic8 = NGX_LOG_INFO,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("warn"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_js_ext_log,
            .magic8 = NGX_LOG_WARN,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("error"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_js_ext_log,
            .magic8 = NGX_LOG_ERR,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("on"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_stream_js_ext_on,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("off"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_stream_js_ext_off,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("send"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_stream_js_ext_send,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("setReturnValue"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = ngx_stream_js_ext_set_return_value,
        }
    },

};


static njs_vm_ops_t ngx_stream_js_ops = {
    ngx_stream_js_set_timer,
    ngx_stream_js_clear_timer
};


static uintptr_t ngx_stream_js_uptr[] = {
    offsetof(ngx_stream_session_t, connection),
    (uintptr_t) ngx_stream_js_pool,
    (uintptr_t) ngx_stream_js_resolver,
    (uintptr_t) ngx_stream_js_resolver_timeout,
    (uintptr_t) ngx_stream_js_handle_event,
    (uintptr_t) ngx_stream_js_ssl,
};


static njs_vm_meta_t ngx_stream_js_metas = {
    .size = 6,
    .values = ngx_stream_js_uptr
};


static ngx_stream_filter_pt  ngx_stream_next_filter;


static njs_int_t    ngx_stream_js_session_proto_id;


static ngx_int_t
ngx_stream_js_access_handler(ngx_stream_session_t *s)
{
    ngx_stream_js_srv_conf_t  *jscf;

    ngx_log_debug0(NGX_LOG_DEBUG_STREAM, s->connection->log, 0,
                   "js access handler");

    jscf = ngx_stream_get_module_srv_conf(s, ngx_stream_js_module);

    return ngx_stream_js_phase_handler(s, &jscf->access);
}


static ngx_int_t
ngx_stream_js_preread_handler(ngx_stream_session_t *s)
{
    ngx_stream_js_srv_conf_t  *jscf;

    ngx_log_debug0(NGX_LOG_DEBUG_STREAM, s->connection->log, 0,
                   "js preread handler");

    jscf = ngx_stream_get_module_srv_conf(s, ngx_stream_js_module);

    return ngx_stream_js_phase_handler(s, &jscf->preread);
}


static ngx_int_t
ngx_stream_js_phase_handler(ngx_stream_session_t *s, ngx_str_t *name)
{
    ngx_str_t             exception;
    njs_int_t             ret;
    ngx_int_t             rc;
    ngx_connection_t     *c;
    ngx_stream_js_ctx_t  *ctx;

    if (name->len == 0) {
        return NGX_DECLINED;
    }

    rc = ngx_stream_js_init_vm(s);
    if (rc != NGX_OK) {
        return rc;
    }

    c = s->connection;

    ngx_log_debug1(NGX_LOG_DEBUG_STREAM, c->log, 0,
                   "stream js phase call \"%V\"", name);

    ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);

    if (!ctx->in_progress) {
        /*
         * status is expected to be overriden by allow(), deny(), decline() or
         * done() methods.
         */

        ctx->status = NGX_ERROR;

        rc = ngx_js_call(ctx->vm, name, c->log, &ctx->args[0], 1);

        if (rc == NGX_ERROR) {
            return rc;
        }
    }

    ret = ngx_stream_js_run_event(s, ctx, &ctx->events[NGX_JS_EVENT_UPLOAD]);
    if (ret != NJS_OK) {
        ngx_js_retval(ctx->vm, NULL, &exception);

        ngx_log_error(NGX_LOG_ERR, c->log, 0, "js exception: %V",
                      &exception);

        return NGX_ERROR;
    }

    if (njs_vm_pending(ctx->vm)) {
        ctx->in_progress = 1;
        rc = ctx->events[NGX_JS_EVENT_UPLOAD].ev ? NGX_AGAIN : NGX_DONE;

    } else {
        ctx->in_progress = 0;
        rc = ctx->status;
    }

    ngx_log_debug1(NGX_LOG_DEBUG_STREAM, c->log, 0, "stream js phase rc: %i",
                   rc);

    return rc;
}


#define ngx_stream_event(from_upstream)                                 \
    (from_upstream ? &ctx->events[NGX_JS_EVENT_DOWNLOAD]                \
                   : &ctx->events[NGX_JS_EVENT_UPLOAD])


static ngx_int_t
ngx_stream_js_body_filter(ngx_stream_session_t *s, ngx_chain_t *in,
    ngx_uint_t from_upstream)
{
    ngx_str_t                  exception;
    njs_int_t                  ret;
    ngx_int_t                  rc;
    ngx_chain_t               *out, *cl, **busy;
    ngx_connection_t          *c, *dst;
    ngx_stream_js_ev_t        *event;
    ngx_stream_js_ctx_t       *ctx;
    ngx_stream_js_srv_conf_t  *jscf;

    jscf = ngx_stream_get_module_srv_conf(s, ngx_stream_js_module);
    if (jscf->filter.len == 0) {
        return ngx_stream_next_filter(s, in, from_upstream);
    }

    c = s->connection;

    ngx_log_debug1(NGX_LOG_DEBUG_STREAM, c->log, 0, "stream js filter u:%ui",
                   from_upstream);

    rc = ngx_stream_js_init_vm(s);

    if (rc == NGX_ERROR) {
        return NGX_ERROR;
    }

    if (rc == NGX_DECLINED) {
        return ngx_stream_next_filter(s, in, from_upstream);
    }

    ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);

    if (!ctx->filter) {
        rc = ngx_js_call(ctx->vm, &jscf->filter, c->log, &ctx->args[0], 1);

        if (rc == NGX_ERROR) {
            return rc;
        }
    }

    ctx->filter = 1;
    ctx->from_upstream = from_upstream;

    ctx->last_out = &out;

    while (in) {
        ctx->buf = in->buf;

        event = ngx_stream_event(from_upstream);

        if (event->ev != NULL) {
            ret = ngx_stream_js_run_event(s, ctx, event);
            if (ret != NJS_OK) {
                ngx_js_retval(ctx->vm, NULL, &exception);

                ngx_log_error(NGX_LOG_ERR, c->log, 0, "js exception: %V",
                              &exception);

                return NGX_ERROR;
            }

            ctx->buf->pos = ctx->buf->last;

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

            cl->buf = ctx->buf;

            *ctx->last_out = cl;
            ctx->last_out = &cl->next;
        }

        in = in->next;
    }

    *ctx->last_out = NULL;

    if (from_upstream) {
        dst = c;
        busy = &ctx->downstream_busy;

    } else {
        dst = s->upstream ? s->upstream->peer.connection : NULL;
        busy = &ctx->upstream_busy;
    }

    if (out != NULL || dst == NULL || dst->buffered) {
        rc = ngx_stream_next_filter(s, out, from_upstream);

        ngx_chain_update_chains(c->pool, &ctx->free, busy, &out,
                                (ngx_buf_tag_t) &ngx_stream_js_module);

    } else {
        rc = NGX_OK;
    }

    return rc;
}


static ngx_int_t
ngx_stream_js_variable_set(ngx_stream_session_t *s,
    ngx_stream_variable_value_t *v, uintptr_t data)
{
    ngx_str_t *fname = (ngx_str_t *) data;

    ngx_int_t             rc;
    njs_int_t             pending;
    ngx_str_t             value;
    ngx_stream_js_ctx_t  *ctx;

    rc = ngx_stream_js_init_vm(s);

    if (rc == NGX_ERROR) {
        return NGX_ERROR;
    }

    if (rc == NGX_DECLINED) {
        v->not_found = 1;
        return NGX_OK;
    }

    ngx_log_debug1(NGX_LOG_DEBUG_STREAM, s->connection->log, 0,
                   "stream js variable call \"%V\"", fname);

    ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);

    pending = njs_vm_pending(ctx->vm);

    rc = ngx_js_call(ctx->vm, fname, s->connection->log, &ctx->args[0], 1);

    if (rc == NGX_ERROR) {
        v->not_found = 1;
        return NGX_OK;
    }

    if (!pending && rc == NGX_AGAIN) {
        ngx_log_error(NGX_LOG_ERR, s->connection->log, 0,
                      "async operation inside \"%V\" variable handler", fname);
        return NGX_ERROR;
    }

    if (ngx_js_retval(ctx->vm, &ctx->retval, &value) != NGX_OK) {
        return NGX_ERROR;
    }

    v->len = value.len;
    v->valid = 1;
    v->no_cacheable = 0;
    v->not_found = 0;
    v->data = value.data;

    return NGX_OK;
}


static ngx_int_t
ngx_stream_js_variable_var(ngx_stream_session_t *s,
    ngx_stream_variable_value_t *v, uintptr_t data)
{
    ngx_stream_complex_value_t *cv = (ngx_stream_complex_value_t *) data;

    ngx_str_t  value;

    if (cv != NULL) {
        if (ngx_stream_complex_value(s, cv, &value) != NGX_OK) {
            return NGX_ERROR;
        }

    } else {
        ngx_str_null(&value);
    }

    v->len = value.len;
    v->valid = 1;
    v->no_cacheable = 0;
    v->not_found = 0;
    v->data = value.data;

    return NGX_OK;
}


static ngx_int_t
ngx_stream_js_init_vm(ngx_stream_session_t *s)
{
    njs_int_t                   rc;
    ngx_str_t                   exception;
    ngx_pool_cleanup_t         *cln;
    ngx_stream_js_ctx_t        *ctx;
    ngx_stream_js_main_conf_t  *jmcf;

    jmcf = ngx_stream_get_module_main_conf(s, ngx_stream_js_module);
    if (jmcf->vm == NULL) {
        return NGX_DECLINED;
    }

    ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);

    if (ctx == NULL) {
        ctx = ngx_pcalloc(s->connection->pool, sizeof(ngx_stream_js_ctx_t));
        if (ctx == NULL) {
            return NGX_ERROR;
        }

        njs_value_invalid_set(njs_value_arg(&ctx->retval));

        ngx_stream_set_ctx(s, ctx, ngx_stream_js_module);
    }

    if (ctx->vm) {
        return NGX_OK;
    }

    ctx->vm = njs_vm_clone(jmcf->vm, s);
    if (ctx->vm == NULL) {
        return NGX_ERROR;
    }

    cln = ngx_pool_cleanup_add(s->connection->pool, 0);
    if (cln == NULL) {
        return NGX_ERROR;
    }

    cln->handler = ngx_stream_js_cleanup;
    cln->data = s;

    if (njs_vm_start(ctx->vm) == NJS_ERROR) {
        ngx_js_retval(ctx->vm, NULL, &exception);

        ngx_log_error(NGX_LOG_ERR, s->connection->log, 0,
                      "js exception: %V", &exception);

        return NGX_ERROR;
    }

    rc = njs_vm_external_create(ctx->vm, njs_value_arg(&ctx->args[0]),
                                ngx_stream_js_session_proto_id, s, 0);
    if (rc != NJS_OK) {
        return NGX_ERROR;
    }

    return NGX_OK;
}


static void
ngx_stream_js_drop_events(ngx_stream_js_ctx_t *ctx)
{
    ngx_uint_t  i;

    for (i = 0; i < NGX_JS_EVENT_MAX; i++) {
        if (ctx->events[i].ev != NULL) {
            njs_vm_del_event(ctx->vm, ctx->events[i].ev);
            ctx->events[i].ev = NULL;
        }
    }
}


static void
ngx_stream_js_cleanup(void *data)
{
    ngx_stream_js_ctx_t  *ctx;

    ngx_stream_session_t *s = data;

    ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);

    ngx_stream_js_drop_events(ctx);

    if (njs_vm_pending(ctx->vm)) {
        ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "pending events");
    }

    njs_vm_destroy(ctx->vm);
}


static void
ngx_stream_js_cleanup_vm(void *data)
{
    njs_vm_t *vm = data;

    njs_vm_destroy(vm);
}


static njs_int_t
ngx_stream_js_run_event(ngx_stream_session_t *s, ngx_stream_js_ctx_t *ctx,
    ngx_stream_js_ev_t *event)
{
    size_t               len;
    u_char              *p;
    njs_int_t            ret;
    ngx_buf_t           *b;
    ngx_connection_t    *c;
    njs_opaque_value_t   last_key, last;

    static const njs_str_t last_str = njs_str("last");

    if (event->ev == NULL) {
        return NJS_OK;
    }

    c = s->connection;
    b = ctx->filter ? ctx->buf : c->buffer;

    len = b ? b->last - b->pos : 0;

    p = ngx_pnalloc(c->pool, len);
    if (p == NULL) {
        njs_vm_memory_error(ctx->vm);
        return NJS_ERROR;
    }

    if (len) {
        ngx_memcpy(p, b->pos, len);
    }

    ret = ngx_js_prop(ctx->vm, event->data_type, njs_value_arg(&ctx->args[1]),
                      p, len);
    if (ret != NJS_OK) {
        return ret;
    }

    njs_vm_value_string_set(ctx->vm, njs_value_arg(&last_key), last_str.start,
                            last_str.length);

    njs_value_boolean_set(njs_value_arg(&last), b && b->last_buf);

    ret = njs_vm_object_alloc(ctx->vm, njs_value_arg(&ctx->args[2]),
                               njs_value_arg(&last_key),
                               njs_value_arg(&last), NULL);
    if (ret != NJS_OK) {
        return ret;
    }

    njs_vm_post_event(ctx->vm, event->ev, njs_value_arg(&ctx->args[1]), 2);

    ret = njs_vm_run(ctx->vm);
    if (ret == NJS_ERROR) {
        return ret;
    }

    return NJS_OK;
}


static njs_vm_event_t *
ngx_stream_js_event(ngx_stream_session_t *s, njs_str_t *event)
{
    ngx_uint_t            i, n, type;
    ngx_stream_js_ctx_t  *ctx;

    static const struct {
        ngx_str_t   name;
        ngx_uint_t  data_type;
        ngx_uint_t  id;
    } events[] = {
        {
            ngx_string("upload"),
            NGX_JS_STRING,
            NGX_JS_EVENT_UPLOAD,
        },

        {
            ngx_string("download"),
            NGX_JS_STRING,
            NGX_JS_EVENT_DOWNLOAD,
        },

        {
            ngx_string("upstream"),
            NGX_JS_BUFFER,
            NGX_JS_EVENT_UPLOAD,
        },

        {
            ngx_string("downstream"),
            NGX_JS_BUFFER,
            NGX_JS_EVENT_DOWNLOAD,
        },
    };

    ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);

    i = 0;
    n = sizeof(events) / sizeof(events[0]);

    while (i < n) {
        if (event->length == events[i].name.len
            && ngx_memcmp(event->start, events[i].name.data, event->length)
               == 0)
        {
            break;
        }

        i++;
    }

    if (i == n) {
        njs_vm_error(ctx->vm, "unknown event \"%V\"", event);
        return NULL;
    }

    ctx->events[events[i].id].data_type = events[i].data_type;

    for (n = 0; n < NGX_JS_EVENT_MAX; n++) {
        type = ctx->events[n].data_type;
        if (type != NGX_JS_UNSET && type != events[i].data_type) {
            njs_vm_error(ctx->vm, "mixing string and buffer events"
                         " is not allowed");
            return NULL;
        }
    }

    return &ctx->events[events[i].id].ev;
}


static njs_int_t
ngx_stream_js_ext_get_remote_address(njs_vm_t *vm,
    njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval,
    njs_value_t *retval)
{
    ngx_connection_t      *c;
    ngx_stream_session_t  *s;

    s = njs_vm_external(vm, ngx_stream_js_session_proto_id, value);
    if (s == NULL) {
        njs_value_undefined_set(retval);
        return NJS_DECLINED;
    }

    c = s->connection;

    return njs_vm_value_string_set(vm, retval, c->addr_text.data,
                                   c->addr_text.len);
}


static njs_int_t
ngx_stream_js_ext_done(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t magic)
{
    ngx_int_t              status;
    njs_value_t           *code;
    ngx_stream_js_ctx_t   *ctx;
    ngx_stream_session_t  *s;

    s = njs_vm_external(vm, ngx_stream_js_session_proto_id,
                        njs_argument(args, 0));
    if (s == NULL) {
        njs_vm_error(vm, "\"this\" is not an external");
        return NJS_ERROR;
    }

    status = (ngx_int_t) magic;
    status = -status;

    if (status == NGX_DONE) {
        status = NGX_STREAM_FORBIDDEN;
    }

    code = njs_arg(args, nargs, 1);

    if (!njs_value_is_undefined(code)) {
        if (ngx_js_integer(vm, code, &status) != NGX_OK) {
            return NJS_ERROR;
        }

        if (status < NGX_ABORT || status > NGX_STREAM_SERVICE_UNAVAILABLE) {
            njs_vm_error(vm, "code is out of range");
            return NJS_ERROR;
        }
    }


    ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);

    ngx_log_debug1(NGX_LOG_DEBUG_STREAM, s->connection->log, 0,
                   "stream js set status: %i", status);

    ctx->status = status;

    ngx_stream_js_drop_events(ctx);

    njs_value_undefined_set(njs_vm_retval(vm));

    return NJS_OK;
}


static njs_int_t
ngx_stream_js_ext_on(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    njs_str_t              name;
    njs_value_t           *callback;
    njs_vm_event_t        *event;
    ngx_stream_session_t  *s;

    s = njs_vm_external(vm, ngx_stream_js_session_proto_id,
                        njs_argument(args, 0));
    if (s == NULL) {
        njs_vm_error(vm, "\"this\" is not an external");
        return NJS_ERROR;
    }

    if (ngx_js_string(vm, njs_arg(args, nargs, 1), &name) == NJS_ERROR) {
        njs_vm_error(vm, "failed to convert event arg");
        return NJS_ERROR;
    }

    callback = njs_arg(args, nargs, 2);
    if (!njs_value_is_function(callback)) {
        njs_vm_error(vm, "callback is not a function");
        return NJS_ERROR;
    }

    event = ngx_stream_js_event(s, &name);
    if (event == NULL) {
        return NJS_ERROR;
    }

    if (*event != NULL) {
        njs_vm_error(vm, "event handler \"%V\" is already set", &name);
        return NJS_ERROR;
    }

    *event = njs_vm_add_event(vm, njs_value_function(callback), 0, NULL, NULL);
    if (*event == NULL) {
        njs_vm_error(vm, "internal error");
        return NJS_ERROR;
    }

    njs_value_undefined_set(njs_vm_retval(vm));

    return NJS_OK;
}


static njs_int_t
ngx_stream_js_ext_off(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    njs_str_t              name;
    njs_vm_event_t        *event;
    ngx_stream_session_t  *s;

    s = njs_vm_external(vm, ngx_stream_js_session_proto_id,
                        njs_argument(args, 0));
    if (s == NULL) {
        njs_vm_error(vm, "\"this\" is not an external");
        return NJS_ERROR;
    }

    if (ngx_js_string(vm, njs_arg(args, nargs, 1), &name) == NJS_ERROR) {
        njs_vm_error(vm, "failed to convert event arg");
        return NJS_ERROR;
    }

    event = ngx_stream_js_event(s, &name);
    if (event == NULL) {
        return NJS_ERROR;
    }

    njs_vm_del_event(vm, *event);

    *event = NULL;

    njs_value_undefined_set(njs_vm_retval(vm));

    return NJS_OK;
}


static njs_int_t
ngx_stream_js_ext_send(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    unsigned               last_buf, flush;
    njs_str_t              buffer;
    ngx_buf_t             *b;
    njs_value_t           *flags, *value;
    ngx_chain_t           *cl;
    ngx_connection_t      *c;
    njs_opaque_value_t     lvalue;
    ngx_stream_js_ctx_t   *ctx;
    ngx_stream_session_t  *s;

    static const njs_str_t last_key = njs_str("last");
    static const njs_str_t flush_key = njs_str("flush");

    s = njs_vm_external(vm, ngx_stream_js_session_proto_id,
                        njs_argument(args, 0));
    if (s == NULL) {
        njs_vm_error(vm, "\"this\" is not an external");
        return NJS_ERROR;
    }

    c = s->connection;

    ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);

    if (!ctx->filter) {
        njs_vm_error(vm, "cannot send buffer in this handler");
        return NJS_ERROR;
    }

    if (ngx_js_string(vm, njs_arg(args, nargs, 1), &buffer) != NGX_OK) {
        njs_vm_error(vm, "failed to get buffer arg");
        return NJS_ERROR;
    }

    flush = ctx->buf->flush;
    last_buf = ctx->buf->last_buf;

    flags = njs_arg(args, nargs, 2);

    if (njs_value_is_object(flags)) {
        value = njs_vm_object_prop(vm, flags, &flush_key, &lvalue);
        if (value != NULL) {
            flush = njs_value_bool(value);
        }

        value = njs_vm_object_prop(vm, flags, &last_key, &lvalue);
        if (value != NULL) {
            last_buf = njs_value_bool(value);
        }
    }

    cl = ngx_chain_get_free_buf(c->pool, &ctx->free);
    if (cl == NULL) {
        njs_vm_error(vm, "memory error");
        return NJS_ERROR;
    }

    b = cl->buf;

    b->flush = flush;
    b->last_buf = last_buf;

    b->memory = (buffer.length ? 1 : 0);
    b->sync = (buffer.length ? 0 : 1);
    b->tag = (ngx_buf_tag_t) &ngx_stream_js_module;

    b->start = buffer.start;
    b->end = buffer.start + buffer.length;
    b->pos = b->start;
    b->last = b->end;

    *ctx->last_out = cl;
    ctx->last_out = &cl->next;

    njs_value_undefined_set(njs_vm_retval(vm));

    return NJS_OK;
}


static njs_int_t
ngx_stream_js_ext_set_return_value(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused)
{
    ngx_stream_js_ctx_t   *ctx;
    ngx_stream_session_t  *s;

    s = njs_vm_external(vm, ngx_stream_js_session_proto_id,
                        njs_argument(args, 0));
    if (s == NULL) {
        njs_vm_error(vm, "\"this\" is not an external");
        return NJS_ERROR;
    }

    ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);

    njs_value_assign(&ctx->retval, njs_arg(args, nargs, 1));
    njs_value_undefined_set(njs_vm_retval(vm));

    return NJS_OK;
}


static njs_int_t
ngx_stream_js_ext_variables(njs_vm_t *vm, njs_object_prop_t *prop,
    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
{
    njs_int_t                     rc;
    njs_str_t                     val;
    ngx_str_t                     name;
    ngx_uint_t                    key;
    ngx_stream_variable_t        *v;
    ngx_stream_session_t         *s;
    ngx_stream_core_main_conf_t  *cmcf;
    ngx_stream_variable_value_t  *vv;

    s = njs_vm_external(vm, ngx_stream_js_session_proto_id, value);
    if (s == NULL) {
        njs_value_undefined_set(retval);
        return NJS_DECLINED;
    }

    rc = njs_vm_prop_name(vm, prop, &val);
    if (rc != NJS_OK) {
        njs_value_undefined_set(retval);
        return NJS_DECLINED;
    }

    name.data = val.start;
    name.len = val.length;

    if (setval == NULL) {
        key = ngx_hash_strlow(name.data, name.data, name.len);

        vv = ngx_stream_get_variable(s, &name, key);
        if (vv == NULL || vv->not_found) {
            njs_value_undefined_set(retval);
            return NJS_DECLINED;
        }

        return ngx_js_prop(vm, njs_vm_prop_magic32(prop), retval, vv->data,
                           vv->len);
    }

    cmcf = ngx_stream_get_module_main_conf(s, ngx_stream_core_module);

    key = ngx_hash_strlow(name.data, name.data, name.len);

    v = ngx_hash_find(&cmcf->variables_hash, key, name.data, name.len);

    if (v == NULL) {
        njs_vm_error(vm, "variable not found");
        return NJS_ERROR;
    }

    if (ngx_js_string(vm, setval, &val) != NGX_OK) {
        return NJS_ERROR;
    }

    if (v->set_handler != NULL) {
        vv = ngx_pcalloc(s->connection->pool,
                         sizeof(ngx_stream_variable_value_t));
        if (vv == NULL) {
            return NJS_ERROR;
        }

        vv->valid = 1;
        vv->not_found = 0;
        vv->data = val.start;
        vv->len = val.length;

        v->set_handler(s, vv, v->data);

        return NJS_OK;
    }

    if (!(v->flags & NGX_STREAM_VAR_INDEXED)) {
        njs_vm_error(vm, "variable is not writable");
        return NJS_ERROR;
    }

    vv = &s->variables[v->index];

    vv->valid = 1;
    vv->not_found = 0;

    vv->data = ngx_pnalloc(s->connection->pool, val.length);
    if (vv->data == NULL) {
        return NJS_ERROR;
    }

    vv->len = val.length;
    ngx_memcpy(vv->data, val.start, vv->len);

    return NJS_OK;
}


static njs_host_event_t
ngx_stream_js_set_timer(njs_external_ptr_t external, uint64_t delay,
    njs_vm_event_t vm_event)
{
    ngx_event_t            *ev;
    ngx_stream_session_t   *s;
    ngx_stream_js_event_t  *js_event;

    s = (ngx_stream_session_t *) external;

    ev = ngx_pcalloc(s->connection->pool, sizeof(ngx_event_t));
    if (ev == NULL) {
        return NULL;
    }

    js_event = ngx_palloc(s->connection->pool, sizeof(ngx_stream_js_event_t));
    if (js_event == NULL) {
        return NULL;
    }

    js_event->session = s;
    js_event->vm_event = vm_event;
    js_event->ident = s->connection->fd;

    ev->data = js_event;
    ev->log = s->connection->log;
    ev->handler = ngx_stream_js_timer_handler;

    ngx_add_timer(ev, delay);

    return ev;
}


static void
ngx_stream_js_clear_timer(njs_external_ptr_t external, njs_host_event_t event)
{
    ngx_event_t  *ev = event;

    if (ev->timer_set) {
        ngx_del_timer(ev);
    }
}


static void
ngx_stream_js_timer_handler(ngx_event_t *ev)
{
    ngx_stream_session_t   *s;
    ngx_stream_js_event_t  *js_event;

    js_event = (ngx_stream_js_event_t *) ev->data;

    s = js_event->session;

    ngx_stream_js_handle_event(s, js_event->vm_event, NULL, 0);
}


static ngx_pool_t *
ngx_stream_js_pool(njs_vm_t *vm, ngx_stream_session_t *s)
{
    return s->connection->pool;
}


static ngx_resolver_t *
ngx_stream_js_resolver(njs_vm_t *vm, ngx_stream_session_t *s)
{
    ngx_stream_core_srv_conf_t  *cscf;

    cscf = ngx_stream_get_module_srv_conf(s, ngx_stream_core_module);

    return cscf->resolver;
}


static ngx_msec_t
ngx_stream_js_resolver_timeout(njs_vm_t *vm, ngx_stream_session_t *s)
{
    ngx_stream_core_srv_conf_t  *cscf;

    cscf = ngx_stream_get_module_srv_conf(s, ngx_stream_core_module);

    return cscf->resolver_timeout;
}


static void
ngx_stream_js_handle_event(ngx_stream_session_t *s, njs_vm_event_t vm_event,
    njs_value_t *args, njs_uint_t nargs)
{
    njs_int_t            rc;
    ngx_str_t            exception;
    ngx_stream_js_ctx_t  *ctx;

    ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);

    njs_vm_post_event(ctx->vm, vm_event, args, nargs);

    rc = njs_vm_run(ctx->vm);

    if (rc == NJS_ERROR) {
        ngx_js_retval(ctx->vm, NULL, &exception);

        ngx_log_error(NGX_LOG_ERR, s->connection->log, 0,
                      "js exception: %V", &exception);

        ngx_stream_finalize_session(s, NGX_STREAM_INTERNAL_SERVER_ERROR);
    }

    if (rc == NJS_OK) {
        ngx_post_event(s->connection->read, &ngx_posted_events);
    }
}


static char *
ngx_stream_js_init_main_conf(ngx_conf_t *cf, void *conf)
{
    ngx_stream_js_main_conf_t *jmcf = conf;

    size_t                   size;
    u_char                  *start, *end, *p;
    ssize_t                  n;
    ngx_fd_t                 fd;
    ngx_str_t               *m, file;
    njs_int_t                rc;
    njs_str_t                text, path;
    ngx_uint_t               i;
    njs_value_t             *value;
    njs_vm_opt_t             options;
    ngx_file_info_t          fi;
    ngx_pool_cleanup_t      *cln;
    njs_opaque_value_t       lvalue, exception;
    ngx_stream_js_import_t  *import;

    static const njs_str_t line_number_key = njs_str("lineNumber");
    static const njs_str_t file_name_key = njs_str("fileName");

    if (jmcf->include.len == 0 && jmcf->imports == NGX_CONF_UNSET_PTR) {
        return NGX_CONF_OK;
    }

    size = 0;
    fd = NGX_INVALID_FILE;

    if (jmcf->include.len != 0) {
        file = jmcf->include;

        if (ngx_conf_full_name(cf->cycle, &file, 1) != NGX_OK) {
            return NGX_CONF_ERROR;
        }

        fd = ngx_open_file(file.data, NGX_FILE_RDONLY, NGX_FILE_OPEN, 0);
        if (fd == NGX_INVALID_FILE) {
            ngx_log_error(NGX_LOG_EMERG, cf->log, ngx_errno,
                          ngx_open_file_n " \"%s\" failed", file.data);
            return NGX_CONF_ERROR;
        }

        if (ngx_fd_info(fd, &fi) == NGX_FILE_ERROR) {
            ngx_log_error(NGX_LOG_EMERG, cf->log, ngx_errno,
                          ngx_fd_info_n " \"%s\" failed", file.data);
            (void) ngx_close_file(fd);
            return NGX_CONF_ERROR;
        }

        size = ngx_file_size(&fi);

    } else {
        import = jmcf->imports->elts;
        for (i = 0; i < jmcf->imports->nelts; i++) {
            size += sizeof("import  from '';\n") - 1 + import[i].name.len
                    + import[i].path.len;
        }
    }

    start = ngx_pnalloc(cf->pool, size);
    if (start == NULL) {
        if (fd != NGX_INVALID_FILE) {
            (void) ngx_close_file(fd);
        }

        return NGX_CONF_ERROR;
    }

    if (jmcf->include.len != 0) {
        n = ngx_read_fd(fd, start, size);

        if (n == -1) {
            ngx_log_error(NGX_LOG_EMERG, cf->log, ngx_errno,
                          ngx_read_fd_n " \"%s\" failed", file.data);

            (void) ngx_close_file(fd);
            return NGX_CONF_ERROR;
        }

        if ((size_t) n != size) {
            ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                          ngx_read_fd_n " has read only %z "
                          "of %O from \"%s\"", n, size, file.data);

            (void) ngx_close_file(fd);
            return NGX_CONF_ERROR;
        }

        if (ngx_close_file(fd) == NGX_FILE_ERROR) {
            ngx_log_error(NGX_LOG_EMERG, cf->log, ngx_errno,
                          ngx_close_file_n " %s failed", file.data);
        }

    } else {
        p = start;
        import = jmcf->imports->elts;
        for (i = 0; i < jmcf->imports->nelts; i++) {
            p = ngx_cpymem(p, "import ", sizeof("import ") - 1);
            p = ngx_cpymem(p, import[i].name.data, import[i].name.len);
            p = ngx_cpymem(p, " from '", sizeof(" from '") - 1);
            p = ngx_cpymem(p, import[i].path.data, import[i].path.len);
            p = ngx_cpymem(p, "';\n", sizeof("';\n") - 1);
        }
    }

    njs_vm_opt_init(&options);

    options.backtrace = 1;
    options.unhandled_rejection = NJS_VM_OPT_UNHANDLED_REJECTION_THROW;
    options.ops = &ngx_stream_js_ops;
    options.metas = &ngx_stream_js_metas;
    options.argv = ngx_argv;
    options.argc = ngx_argc;

    if (jmcf->include.len != 0) {
        file = jmcf->include;

    } else {
        file = ngx_cycle->conf_prefix;
    }

    options.file.start = file.data;
    options.file.length = file.len;

    jmcf->vm = njs_vm_create(&options);
    if (jmcf->vm == NULL) {
        ngx_log_error(NGX_LOG_EMERG, cf->log, 0, "failed to create js VM");
        return NGX_CONF_ERROR;
    }

    cln = ngx_pool_cleanup_add(cf->pool, 0);
    if (cln == NULL) {
        return NGX_CONF_ERROR;
    }

    cln->handler = ngx_stream_js_cleanup_vm;
    cln->data = jmcf->vm;

    path.start = ngx_cycle->conf_prefix.data;
    path.length = ngx_cycle->conf_prefix.len;

    rc = njs_vm_add_path(jmcf->vm, &path);
    if (rc != NJS_OK) {
        ngx_log_error(NGX_LOG_EMERG, cf->log, 0, "failed to add \"js_path\"");
        return NGX_CONF_ERROR;
    }

    if (jmcf->paths != NGX_CONF_UNSET_PTR) {
        m = jmcf->paths->elts;

        for (i = 0; i < jmcf->paths->nelts; i++) {
            if (ngx_conf_full_name(cf->cycle, &m[i], 1) != NGX_OK) {
                return NGX_CONF_ERROR;
            }

            path.start = m[i].data;
            path.length = m[i].len;

            rc = njs_vm_add_path(jmcf->vm, &path);
            if (rc != NJS_OK) {
                ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                              "failed to add \"js_path\"");
                return NGX_CONF_ERROR;
            }
        }
    }

    ngx_stream_js_session_proto_id = njs_vm_external_prototype(jmcf->vm,
                                         ngx_stream_js_ext_session,
                                         njs_nitems(ngx_stream_js_ext_session));
    if (ngx_stream_js_session_proto_id < 0) {
        ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                      "failed to add js request proto");
        return NGX_CONF_ERROR;
    }

    rc = ngx_js_core_init(jmcf->vm, cf->log);
    if (njs_slow_path(rc != NJS_OK)) {
        return NGX_CONF_ERROR;
    }

    end = start + size;

    rc = njs_vm_compile(jmcf->vm, &start, end);

    if (rc != NJS_OK) {
        njs_value_assign(&exception, njs_vm_retval(jmcf->vm));
        njs_vm_retval_string(jmcf->vm, &text);

        if (jmcf->include.len != 0) {
            ngx_log_error(NGX_LOG_EMERG, cf->log, 0, "%*s, included in %s:%ui",
                          text.length, text.start, jmcf->file, jmcf->line);
            return NGX_CONF_ERROR;
        }

        value = njs_vm_object_prop(jmcf->vm, njs_value_arg(&exception),
                                   &file_name_key, &lvalue);
        if (value == NULL) {
            value = njs_vm_object_prop(jmcf->vm, njs_value_arg(&exception),
                                       &line_number_key, &lvalue);

            if (value != NULL) {
                i = njs_value_number(value) - 1;

                if (i < jmcf->imports->nelts) {
                    import = jmcf->imports->elts;
                    ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                                  "%*s, included in %s:%ui", text.length,
                                  text.start, import[i].file, import[i].line);
                    return NGX_CONF_ERROR;
                }
            }
        }

        ngx_log_error(NGX_LOG_EMERG, cf->log, 0, "%*s", text.length,
                      text.start);
        return NGX_CONF_ERROR;
    }

    if (start != end) {
        ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                      "extra characters in js script: \"%*s\"",
                      end - start, start);
        return NGX_CONF_ERROR;
    }

    return NGX_CONF_OK;
}


static char *
ngx_stream_js_include(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_stream_js_main_conf_t *jmcf = conf;

    if (jmcf->imports != NGX_CONF_UNSET_PTR) {
        return "is incompatible with \"js_import\"";
    }

    jmcf->file = cf->conf_file->file.name.data;
    jmcf->line = cf->conf_file->line;

    return ngx_conf_set_str_slot(cf, cmd, conf);
}


static char *
ngx_stream_js_import(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_stream_js_main_conf_t *jmcf = conf;

    u_char                  *p, *end, c;
    ngx_int_t               from;
    ngx_str_t               *value, name, path;
    ngx_stream_js_import_t  *import;

    if (jmcf->include.len != 0) {
        return "is incompatible with \"js_include\"";
    }

    value = cf->args->elts;
    from = (cf->args->nelts == 4);

    if (from) {
        if (ngx_strcmp(value[2].data, "from") != 0) {
            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                               "invalid parameter \"%V\"", &value[2]);
            return NGX_CONF_ERROR;
        }
    }

    name = value[1];
    path = (from ? value[3] : value[1]);

    if (!from) {
        end = name.data + name.len;

        for (p = end - 1; p >= name.data; p--) {
            if (*p == '/') {
                break;
            }
        }

        name.data = p + 1;
        name.len = end - p - 1;

        if (name.len < 3
            || ngx_memcmp(&name.data[name.len - 3], ".js", 3) != 0)
        {
            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                               "cannot extract export name from file path "
                               "\"%V\", use extended \"from\" syntax", &path);
            return NGX_CONF_ERROR;
        }

        name.len -= 3;
    }

    if (name.len == 0) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "empty \"name\" parameter");
        return NGX_CONF_ERROR;
    }

    p = name.data;
    end = name.data + name.len;

    while (p < end) {
        c = ngx_tolower(*p);

        if (*p != '_' && (c < 'a' || c > 'z')) {
            if (p == name.data) {
                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "cannot start "
                                   "with \"%c\" in export name \"%V\"", *p,
                                   &name);
                return NGX_CONF_ERROR;
            }

            if (*p < '0' || *p > '9') {
                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid character "
                                   "\"%c\" in export name \"%V\"", *p, &name);
                return NGX_CONF_ERROR;
            }
        }

        p++;
    }

    if (ngx_strchr(path.data, '\'') != NULL) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid character \"'\" "
                           "in file path \"%V\"", &path);
        return NGX_CONF_ERROR;
    }

    if (jmcf->imports == NGX_CONF_UNSET_PTR) {
        jmcf->imports = ngx_array_create(cf->pool, 4,
                                         sizeof(ngx_stream_js_import_t));
        if (jmcf->imports == NULL) {
            return NGX_CONF_ERROR;
        }
    }

    import = ngx_array_push(jmcf->imports);
    if (import == NULL) {
        return NGX_CONF_ERROR;
    }

    import->name = name;
    import->path = path;
    import->file = cf->conf_file->file.name.data;
    import->line = cf->conf_file->line;

    return NGX_CONF_OK;
}


static char *
ngx_stream_js_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_str_t              *value, *fname;
    ngx_stream_variable_t  *v;

    value = cf->args->elts;

    if (value[1].data[0] != '$') {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                           "invalid variable name \"%V\"", &value[1]);
        return NGX_CONF_ERROR;
    }

    value[1].len--;
    value[1].data++;

    v = ngx_stream_add_variable(cf, &value[1], NGX_STREAM_VAR_CHANGEABLE);
    if (v == NULL) {
        return NGX_CONF_ERROR;
    }

    fname = ngx_palloc(cf->pool, sizeof(ngx_str_t));
    if (fname == NULL) {
        return NGX_CONF_ERROR;
    }

    *fname = value[2];

    v->get_handler = ngx_stream_js_variable_set;
    v->data = (uintptr_t) fname;

    return NGX_CONF_OK;
}


static char *
ngx_stream_js_var(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_str_t                           *value;
    ngx_int_t                            index;
    ngx_stream_variable_t               *v;
    ngx_stream_complex_value_t          *cv;
    ngx_stream_compile_complex_value_t   ccv;

    value = cf->args->elts;

    if (value[1].data[0] != '$') {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                           "invalid variable name \"%V\"", &value[1]);
        return NGX_CONF_ERROR;
    }

    value[1].len--;
    value[1].data++;

    v = ngx_stream_add_variable(cf, &value[1], NGX_STREAM_VAR_CHANGEABLE);
    if (v == NULL) {
        return NGX_CONF_ERROR;
    }

    index = ngx_stream_get_variable_index(cf, &value[1]);
    if (index == NGX_ERROR) {
        return NGX_CONF_ERROR;
    }

    cv = NULL;

    if (cf->args->nelts == 3) {
        cv = ngx_palloc(cf->pool, sizeof(ngx_stream_complex_value_t));
        if (cv == NULL) {
            return NGX_CONF_ERROR;
        }

        ngx_memzero(&ccv, sizeof(ngx_stream_compile_complex_value_t));

        ccv.cf = cf;
        ccv.value = &value[2];
        ccv.complex_value = cv;

        if (ngx_stream_compile_complex_value(&ccv) != NGX_OK) {
            return NGX_CONF_ERROR;
        }
    }

    v->get_handler = ngx_stream_js_variable_var;
    v->data = (uintptr_t) cv;

    return NGX_CONF_OK;
}


static void *
ngx_stream_js_create_main_conf(ngx_conf_t *cf)
{
    ngx_stream_js_main_conf_t  *conf;

    conf = ngx_pcalloc(cf->pool, sizeof(ngx_stream_js_main_conf_t));
    if (conf == NULL) {
        return NULL;
    }

    /*
     * set by ngx_pcalloc():
     *
     *     conf->vm = NULL;
     *     conf->include = { 0, NULL };
     *     conf->file = NULL;
     *     conf->line = 0;
     */

    conf->paths = NGX_CONF_UNSET_PTR;
    conf->imports = NGX_CONF_UNSET_PTR;

    return conf;
}


static void *
ngx_stream_js_create_srv_conf(ngx_conf_t *cf)
{
    ngx_stream_js_srv_conf_t  *conf;

    conf = ngx_pcalloc(cf->pool, sizeof(ngx_stream_js_srv_conf_t));
    if (conf == NULL) {
        return NULL;
    }

    /*
     * set by ngx_pcalloc():
     *
     *     conf->access = { 0, NULL };
     *     conf->preread = { 0, NULL };
     *     conf->filter = { 0, NULL };
     *     conf->ssl_ciphers = { 0, NULL };
     *     conf->ssl_protocols = 0;
     *     conf->ssl_trusted_certificate = { 0, NULL };
     */

#if (NGX_SSL)
    conf->ssl_verify_depth = NGX_CONF_UNSET;
#endif
    return conf;
}


static char *
ngx_stream_js_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_stream_js_srv_conf_t *prev = parent;
    ngx_stream_js_srv_conf_t *conf = child;

    ngx_conf_merge_str_value(conf->access, prev->access, "");
    ngx_conf_merge_str_value(conf->preread, prev->preread, "");
    ngx_conf_merge_str_value(conf->filter, prev->filter, "");

#if (NGX_HTTP_SSL)
    ngx_conf_merge_str_value(conf->ssl_ciphers, prev->ssl_ciphers, "DEFAULT");

    ngx_conf_merge_bitmask_value(conf->ssl_protocols, prev->ssl_protocols,
                                 (NGX_CONF_BITMASK_SET|NGX_SSL_TLSv1
                                  |NGX_SSL_TLSv1_1|NGX_SSL_TLSv1_2));

    ngx_conf_merge_value(conf->ssl_verify_depth, prev->ssl_verify_depth, 100);

    ngx_conf_merge_str_value(conf->ssl_trusted_certificate,
                             prev->ssl_trusted_certificate, "");

    return ngx_stream_js_set_ssl(cf, conf);
#else
    return NGX_CONF_OK;
#endif
}


static ngx_int_t
ngx_stream_js_init(ngx_conf_t *cf)
{
    ngx_stream_handler_pt        *h;
    ngx_stream_core_main_conf_t  *cmcf;

    ngx_stream_next_filter = ngx_stream_top_filter;
    ngx_stream_top_filter = ngx_stream_js_body_filter;

    cmcf = ngx_stream_conf_get_module_main_conf(cf, ngx_stream_core_module);

    h = ngx_array_push(&cmcf->phases[NGX_STREAM_ACCESS_PHASE].handlers);
    if (h == NULL) {
        return NGX_ERROR;
    }

    *h = ngx_stream_js_access_handler;

    h = ngx_array_push(&cmcf->phases[NGX_STREAM_PREREAD_PHASE].handlers);
    if (h == NULL) {
        return NGX_ERROR;
    }

    *h = ngx_stream_js_preread_handler;

    return NGX_OK;
}


#if (NGX_SSL)

static char *
ngx_stream_js_set_ssl(ngx_conf_t *cf, ngx_stream_js_srv_conf_t *jscf)
{
    ngx_ssl_t           *ssl;
    ngx_pool_cleanup_t  *cln;

    ssl = ngx_pcalloc(cf->pool, sizeof(ngx_ssl_t));
    if (ssl == NULL) {
        return NGX_CONF_ERROR;
    }

    jscf->ssl = ssl;
    ssl->log = cf->log;

    if (ngx_ssl_create(ssl, jscf->ssl_protocols, NULL) != NGX_OK) {
        return NGX_CONF_ERROR;
    }

    cln = ngx_pool_cleanup_add(cf->pool, 0);
    if (cln == NULL) {
        ngx_ssl_cleanup_ctx(ssl);
        return NGX_CONF_ERROR;
    }

    cln->handler = ngx_ssl_cleanup_ctx;
    cln->data = ssl;

    if (ngx_ssl_ciphers(NULL, ssl, &jscf->ssl_ciphers, 0) != NGX_OK) {
        return NGX_CONF_ERROR;
    }

    if (ngx_ssl_trusted_certificate(cf, ssl, &jscf->ssl_trusted_certificate,
                                    jscf->ssl_verify_depth)
        != NGX_OK)
    {
        return NGX_CONF_ERROR;
    }

    return NGX_CONF_OK;
}

#endif


static ngx_ssl_t *
ngx_stream_js_ssl(njs_vm_t *vm, ngx_stream_session_t *s)
{
#if (NGX_SSL)
    ngx_stream_js_srv_conf_t  *jscf;

    jscf = ngx_stream_get_module_srv_conf(s, ngx_stream_js_module);

    return jscf->ssl;
#else
    return NULL;
#endif
}
