diff options
Diffstat (limited to '')
-rw-r--r-- | web/server/h2o/libh2o/lib/handler/mruby.c | 938 | ||||
-rw-r--r-- | web/server/h2o/libh2o/lib/handler/mruby/chunked.c | 270 | ||||
-rw-r--r-- | web/server/h2o/libh2o/lib/handler/mruby/embedded.c.h | 111 | ||||
-rw-r--r-- | web/server/h2o/libh2o/lib/handler/mruby/embedded/chunked.rb | 35 | ||||
-rw-r--r-- | web/server/h2o/libh2o/lib/handler/mruby/embedded/core.rb | 81 | ||||
-rw-r--r-- | web/server/h2o/libh2o/lib/handler/mruby/embedded/http_request.rb | 54 | ||||
-rw-r--r-- | web/server/h2o/libh2o/lib/handler/mruby/http_request.c | 500 |
7 files changed, 1989 insertions, 0 deletions
diff --git a/web/server/h2o/libh2o/lib/handler/mruby.c b/web/server/h2o/libh2o/lib/handler/mruby.c new file mode 100644 index 000000000..af2af53fd --- /dev/null +++ b/web/server/h2o/libh2o/lib/handler/mruby.c @@ -0,0 +1,938 @@ +/* + * Copyright (c) 2014-2016 DeNA Co., Ltd., Kazuho Oku, Ryosuke Matsumoto, + * Masayoshi Takahashi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +#include <errno.h> +#include <inttypes.h> +#include <stdio.h> +#include <stdlib.h> +#include <mruby.h> +#include <mruby/proc.h> +#include <mruby/array.h> +#include <mruby/class.h> +#include <mruby/compile.h> +#include <mruby/error.h> +#include <mruby/hash.h> +#include <mruby/string.h> +#include <mruby/throw.h> +#include <mruby/variable.h> +#include <mruby_input_stream.h> +#include "h2o.h" +#include "h2o/mruby_.h" +#include "mruby/embedded.c.h" + +#define STATUS_FALLTHRU 399 +#define FALLTHRU_SET_PREFIX "x-fallthru-set-" + +#define FREEZE_STRING(v) MRB_SET_FROZEN_FLAG(mrb_obj_ptr(v)) + +__thread h2o_mruby_generator_t *h2o_mruby_current_generator = NULL; + +void h2o_mruby__assert_failed(mrb_state *mrb, const char *file, int line) +{ + mrb_value obj = mrb_funcall(mrb, mrb_obj_value(mrb->exc), "inspect", 0); + struct RString *error = mrb_str_ptr(obj); + fprintf(stderr, "unexpected ruby error at file: \"%s\", line %d: %s", file, line, error->as.heap.ptr); + abort(); +} + +void h2o_mruby_setup_globals(mrb_state *mrb) +{ + const char *root = getenv("H2O_ROOT"); + if (root == NULL) + root = H2O_TO_STR(H2O_ROOT); + mrb_gv_set(mrb, mrb_intern_lit(mrb, "$H2O_ROOT"), mrb_str_new(mrb, root, strlen(root))); + + h2o_mruby_eval_expr(mrb, "$LOAD_PATH << \"#{$H2O_ROOT}/share/h2o/mruby\""); + h2o_mruby_assert(mrb); + + /* require core modules and include built-in libraries */ + h2o_mruby_eval_expr(mrb, "require \"#{$H2O_ROOT}/share/h2o/mruby/preloads.rb\""); + if (mrb->exc != NULL) { + if (mrb_obj_is_instance_of(mrb, mrb_obj_value(mrb->exc), mrb_class_get(mrb, "LoadError"))) { + fprintf(stderr, "file \"%s/%s\" not found. Did you forget to run `make install` ?", root, + "share/h2o/mruby/preloads.rb"); + } else { + mrb_value obj = mrb_funcall(mrb, mrb_obj_value(mrb->exc), "inspect", 0); + struct RString *error = mrb_str_ptr(obj); + fprintf(stderr, "an error occurred while loading %s/%s: %s", root, "share/h2o/mruby/preloads.rb", error->as.heap.ptr); + } + abort(); + } +} + +mrb_value h2o_mruby_to_str(mrb_state *mrb, mrb_value v) +{ + if (!mrb_string_p(v)) + H2O_MRUBY_EXEC_GUARD({ v = mrb_str_to_str(mrb, v); }); + return v; +} + +mrb_value h2o_mruby_eval_expr(mrb_state *mrb, const char *expr) +{ + return mrb_funcall(mrb, mrb_top_self(mrb), "eval", 1, mrb_str_new_cstr(mrb, expr)); +} + +void h2o_mruby_define_callback(mrb_state *mrb, const char *name, int id) +{ + mrb_value args[2]; + args[0] = mrb_str_new_cstr(mrb, name); + args[1] = mrb_fixnum_value(id); + mrb_funcall_argv(mrb, mrb_top_self(mrb), mrb_intern_lit(mrb, "_h2o_define_callback"), 2, args); + + if (mrb->exc != NULL) { + fprintf(stderr, "failed to define mruby function: %s\n", name); + h2o_mruby_assert(mrb); + } +} + +mrb_value h2o_mruby_create_data_instance(mrb_state *mrb, mrb_value class_obj, void *ptr, const mrb_data_type *type) +{ + struct RClass *klass = mrb_class_ptr(class_obj); + struct RData *data = mrb_data_object_alloc(mrb, klass, ptr, type); + return mrb_obj_value(data); +} + +mrb_value h2o_mruby_compile_code(mrb_state *mrb, h2o_mruby_config_vars_t *config, char *errbuf) +{ + mrbc_context *cxt; + struct mrb_parser_state *parser; + struct RProc *proc = NULL; + mrb_value result = mrb_nil_value(); + + /* parse */ + if ((cxt = mrbc_context_new(mrb)) == NULL) { + fprintf(stderr, "%s: no memory\n", H2O_MRUBY_MODULE_NAME); + abort(); + } + if (config->path != NULL) + mrbc_filename(mrb, cxt, config->path); + cxt->capture_errors = 1; + cxt->lineno = config->lineno; + if ((parser = mrb_parse_nstring(mrb, config->source.base, (int)config->source.len, cxt)) == NULL) { + fprintf(stderr, "%s: no memory\n", H2O_MRUBY_MODULE_NAME); + abort(); + } + /* return erro if errbuf is supplied, or abort */ + if (parser->nerr != 0) { + if (errbuf == NULL) { + fprintf(stderr, "%s: internal error (unexpected state)\n", H2O_MRUBY_MODULE_NAME); + abort(); + } + snprintf(errbuf, 256, "line %d:%s", parser->error_buffer[0].lineno, parser->error_buffer[0].message); + strcat(errbuf, "\n\n"); + if (h2o_str_at_position(errbuf + strlen(errbuf), config->source.base, config->source.len, + parser->error_buffer[0].lineno - config->lineno + 1, parser->error_buffer[0].column) != 0) { + /* remove trailing "\n\n" in case we failed to append the source code at the error location */ + errbuf[strlen(errbuf) - 2] = '\0'; + } + goto Exit; + } + /* generate code */ + if ((proc = mrb_generate_code(mrb, parser)) == NULL) { + fprintf(stderr, "%s: internal error (mrb_generate_code failed)\n", H2O_MRUBY_MODULE_NAME); + abort(); + } + + /* adjust stack length of toplevel environment (see https://github.com/h2o/h2o/issues/1464#issuecomment-337880408) */ + if (mrb->c->cibase->env) { + struct REnv *e = mrb->c->cibase->env; + if (MRB_ENV_STACK_LEN(e) < proc->body.irep->nlocals) + MRB_SET_ENV_STACK_LEN(e, proc->body.irep->nlocals); + } + + /* reset configuration context */ + h2o_mruby_eval_expr(mrb, "H2O::ConfigurationContext.reset"); + h2o_mruby_assert(mrb); + + /* run code and generate handler */ + result = mrb_run(mrb, proc, mrb_top_self(mrb)); + if (mrb->exc != NULL) { + mrb_value obj = mrb_funcall(mrb, mrb_obj_value(mrb->exc), "inspect", 0); + struct RString *error = mrb_str_ptr(obj); + snprintf(errbuf, 256, "%s", error->as.heap.ptr); + mrb->exc = 0; + result = mrb_nil_value(); + goto Exit; + } else if (mrb_nil_p(result)) { + snprintf(errbuf, 256, "returned value is not callable"); + goto Exit; + } + + /* call post_handler_generation hooks */ + mrb_funcall_argv(mrb, h2o_mruby_eval_expr(mrb, "H2O::ConfigurationContext.instance"), + mrb_intern_lit(mrb, "call_post_handler_generation_hooks"), 1, &result); + if (mrb->exc != NULL) { + mrb_value obj = mrb_funcall(mrb, mrb_obj_value(mrb->exc), "inspect", 0); + struct RString *error = mrb_str_ptr(obj); + snprintf(errbuf, 256, "%s", error->as.heap.ptr); + mrb->exc = 0; + result = mrb_nil_value(); + goto Exit; + } + +Exit: + mrb_parser_free(parser); + mrbc_context_free(mrb, cxt); + return result; +} + +static h2o_iovec_t convert_header_name_to_env(h2o_mem_pool_t *pool, const char *name, size_t len) +{ +#define KEY_PREFIX "HTTP_" +#define KEY_PREFIX_LEN (sizeof(KEY_PREFIX) - 1) + + h2o_iovec_t ret; + + ret.len = len + KEY_PREFIX_LEN; + ret.base = h2o_mem_alloc_pool(pool, ret.len); + + memcpy(ret.base, KEY_PREFIX, KEY_PREFIX_LEN); + + char *d = ret.base + KEY_PREFIX_LEN; + for (; len != 0; ++name, --len) + *d++ = *name == '-' ? '_' : h2o_toupper(*name); + + return ret; + +#undef KEY_PREFIX +#undef KEY_PREFIX_LEN +} + +static mrb_value build_constants(mrb_state *mrb, const char *server_name, size_t server_name_len) +{ + mrb_value ary = mrb_ary_new_capa(mrb, H2O_MRUBY_NUM_CONSTANTS); + mrb_int i; + + int gc_arena = mrb_gc_arena_save(mrb); + + { + h2o_mem_pool_t pool; + h2o_mem_init_pool(&pool); + for (i = 0; i != H2O_MAX_TOKENS; ++i) { + const h2o_token_t *token = h2o__tokens + i; + mrb_value lit = mrb_nil_value(); + if (token == H2O_TOKEN_CONTENT_TYPE) { + lit = mrb_str_new_lit(mrb, "CONTENT_TYPE"); + } else if (token->buf.len != 0) { + h2o_iovec_t n = convert_header_name_to_env(&pool, token->buf.base, token->buf.len); + lit = mrb_str_new(mrb, n.base, n.len); + } + if (mrb_string_p(lit)) { + FREEZE_STRING(lit); + mrb_ary_set(mrb, ary, i, lit); + } + } + h2o_mem_clear_pool(&pool); + } + +#define SET_STRING(idx, value) \ + do { \ + mrb_value lit = (value); \ + FREEZE_STRING(lit); \ + mrb_ary_set(mrb, ary, idx, lit); \ + } while (0) +#define SET_LITERAL(idx, str) SET_STRING(idx, mrb_str_new_lit(mrb, str)) + + SET_LITERAL(H2O_MRUBY_LIT_REQUEST_METHOD, "REQUEST_METHOD"); + SET_LITERAL(H2O_MRUBY_LIT_SCRIPT_NAME, "SCRIPT_NAME"); + SET_LITERAL(H2O_MRUBY_LIT_PATH_INFO, "PATH_INFO"); + SET_LITERAL(H2O_MRUBY_LIT_QUERY_STRING, "QUERY_STRING"); + SET_LITERAL(H2O_MRUBY_LIT_SERVER_NAME, "SERVER_NAME"); + SET_LITERAL(H2O_MRUBY_LIT_SERVER_ADDR, "SERVER_ADDR"); + SET_LITERAL(H2O_MRUBY_LIT_SERVER_PORT, "SERVER_PORT"); + SET_LITERAL(H2O_MRUBY_LIT_SERVER_PROTOCOL, "SERVER_PROTOCOL"); + SET_LITERAL(H2O_MRUBY_LIT_CONTENT_LENGTH, "CONTENT_LENGTH"); + SET_LITERAL(H2O_MRUBY_LIT_REMOTE_ADDR, "REMOTE_ADDR"); + SET_LITERAL(H2O_MRUBY_LIT_REMOTE_PORT, "REMOTE_PORT"); + SET_LITERAL(H2O_MRUBY_LIT_REMOTE_USER, "REMOTE_USER"); + SET_LITERAL(H2O_MRUBY_LIT_RACK_URL_SCHEME, "rack.url_scheme"); + SET_LITERAL(H2O_MRUBY_LIT_RACK_MULTITHREAD, "rack.multithread"); + SET_LITERAL(H2O_MRUBY_LIT_RACK_MULTIPROCESS, "rack.multiprocess"); + SET_LITERAL(H2O_MRUBY_LIT_RACK_RUN_ONCE, "rack.run_once"); + SET_LITERAL(H2O_MRUBY_LIT_RACK_HIJACK_, "rack.hijack?"); + SET_LITERAL(H2O_MRUBY_LIT_RACK_INPUT, "rack.input"); + SET_LITERAL(H2O_MRUBY_LIT_RACK_ERRORS, "rack.errors"); + SET_LITERAL(H2O_MRUBY_LIT_SERVER_SOFTWARE, "SERVER_SOFTWARE"); + SET_STRING(H2O_MRUBY_LIT_SERVER_SOFTWARE_VALUE, mrb_str_new(mrb, server_name, server_name_len)); + SET_LITERAL(H2O_MRUBY_LIT_SEPARATOR_COMMA, ", "); + SET_LITERAL(H2O_MRUBY_LIT_SEPARATOR_SEMICOLON, "; "); + +#undef SET_LITERAL +#undef SET_STRING + + h2o_mruby_eval_expr(mrb, H2O_MRUBY_CODE_CORE); + h2o_mruby_assert(mrb); + + mrb_ary_set(mrb, ary, H2O_MRUBY_PROC_EACH_TO_ARRAY, + mrb_funcall(mrb, mrb_obj_value(mrb->kernel_module), "_h2o_proc_each_to_array", 0)); + h2o_mruby_assert(mrb); + + /* sends exception using H2O_MRUBY_CALLBACK_ID_EXCEPTION_RAISED */ + mrb_ary_set(mrb, ary, H2O_MRUBY_PROC_APP_TO_FIBER, + mrb_funcall(mrb, mrb_obj_value(mrb->kernel_module), "_h2o_proc_app_to_fiber", 0)); + h2o_mruby_assert(mrb); + + mrb_gc_arena_restore(mrb, gc_arena); + return ary; +} + +static h2o_mruby_shared_context_t *create_shared_context(h2o_context_t *ctx) +{ + /* init mruby in every thread */ + h2o_mruby_shared_context_t *shared_ctx = h2o_mem_alloc(sizeof(*shared_ctx)); + if ((shared_ctx->mrb = mrb_open()) == NULL) { + fprintf(stderr, "%s: no memory\n", H2O_MRUBY_MODULE_NAME); + abort(); + } + h2o_mruby_setup_globals(shared_ctx->mrb); + shared_ctx->constants = build_constants(shared_ctx->mrb, ctx->globalconf->server_name.base, ctx->globalconf->server_name.len); + shared_ctx->symbols.sym_call = mrb_intern_lit(shared_ctx->mrb, "call"); + shared_ctx->symbols.sym_close = mrb_intern_lit(shared_ctx->mrb, "close"); + shared_ctx->symbols.sym_method = mrb_intern_lit(shared_ctx->mrb, "method"); + shared_ctx->symbols.sym_headers = mrb_intern_lit(shared_ctx->mrb, "headers"); + shared_ctx->symbols.sym_body = mrb_intern_lit(shared_ctx->mrb, "body"); + shared_ctx->symbols.sym_async = mrb_intern_lit(shared_ctx->mrb, "async"); + + h2o_mruby_send_chunked_init_context(shared_ctx); + h2o_mruby_http_request_init_context(shared_ctx); + + return shared_ctx; +} + +static void dispose_shared_context(void *data) +{ + if (data == NULL) + return; + h2o_mruby_shared_context_t *shared_ctx = (h2o_mruby_shared_context_t *)data; + mrb_close(shared_ctx->mrb); + free(shared_ctx); +} + +static h2o_mruby_shared_context_t *get_shared_context(h2o_context_t *ctx) +{ + static size_t key = SIZE_MAX; + void **data = h2o_context_get_storage(ctx, &key, dispose_shared_context); + if (*data == NULL) { + *data = create_shared_context(ctx); + } + return *data; +} + +static void on_context_init(h2o_handler_t *_handler, h2o_context_t *ctx) +{ + h2o_mruby_handler_t *handler = (void *)_handler; + h2o_mruby_context_t *handler_ctx = h2o_mem_alloc(sizeof(*handler_ctx)); + + handler_ctx->handler = handler; + handler_ctx->shared = get_shared_context(ctx); + + /* compile code (must be done for each thread) */ + int arena = mrb_gc_arena_save(handler_ctx->shared->mrb); + mrb_value proc = h2o_mruby_compile_code(handler_ctx->shared->mrb, &handler->config, NULL); + + handler_ctx->proc = + mrb_funcall_argv(handler_ctx->shared->mrb, mrb_ary_entry(handler_ctx->shared->constants, H2O_MRUBY_PROC_APP_TO_FIBER), + handler_ctx->shared->symbols.sym_call, 1, &proc); + h2o_mruby_assert(handler_ctx->shared->mrb); + mrb_gc_arena_restore(handler_ctx->shared->mrb, arena); + mrb_gc_protect(handler_ctx->shared->mrb, handler_ctx->proc); + + h2o_context_set_handler_context(ctx, &handler->super, handler_ctx); +} + +static void on_context_dispose(h2o_handler_t *_handler, h2o_context_t *ctx) +{ + h2o_mruby_handler_t *handler = (void *)_handler; + h2o_mruby_context_t *handler_ctx = h2o_context_get_handler_context(ctx, &handler->super); + + if (handler_ctx == NULL) + return; + + free(handler_ctx); +} + +static void on_handler_dispose(h2o_handler_t *_handler) +{ + h2o_mruby_handler_t *handler = (void *)_handler; + + free(handler->config.source.base); + free(handler->config.path); + free(handler); +} + +static void report_exception(h2o_req_t *req, mrb_state *mrb) +{ + mrb_value obj = mrb_funcall(mrb, mrb_obj_value(mrb->exc), "inspect", 0); + struct RString *error = mrb_str_ptr(obj); + h2o_req_log_error(req, H2O_MRUBY_MODULE_NAME, "mruby raised: %s\n", error->as.heap.ptr); + mrb->exc = NULL; +} + +static void stringify_address(h2o_conn_t *conn, socklen_t (*cb)(h2o_conn_t *conn, struct sockaddr *), mrb_state *mrb, + mrb_value *host, mrb_value *port) +{ + struct sockaddr_storage ss; + socklen_t sslen; + char buf[NI_MAXHOST]; + + *host = mrb_nil_value(); + *port = mrb_nil_value(); + + if ((sslen = cb(conn, (void *)&ss)) == 0) + return; + size_t l = h2o_socket_getnumerichost((void *)&ss, sslen, buf); + if (l != SIZE_MAX) + *host = mrb_str_new(mrb, buf, l); + int32_t p = h2o_socket_getport((void *)&ss); + if (p != -1) { + l = (int)sprintf(buf, "%" PRIu16, (uint16_t)p); + *port = mrb_str_new(mrb, buf, l); + } +} + +static void on_rack_input_free(mrb_state *mrb, const char *base, mrb_int len, void *_input_stream) +{ + /* reset ref to input_stream */ + mrb_value *input_stream = _input_stream; + *input_stream = mrb_nil_value(); +} + +static int build_env_sort_header_cb(const void *_x, const void *_y) +{ + const h2o_header_t *x = *(const h2o_header_t **)_x, *y = *(const h2o_header_t **)_y; + if (x->name->len < y->name->len) + return -1; + if (x->name->len > y->name->len) + return 1; + if (x->name->base != y->name->base) { + int r = memcmp(x->name->base, y->name->base, x->name->len); + if (r != 0) + return r; + } + assert(x != y); + /* the order of the headers having the same name needs to be retained */ + return x < y ? -1 : 1; +} + +static mrb_value build_path_info(mrb_state *mrb, h2o_req_t *req, size_t confpath_len_wo_slash) +{ + if (req->path_normalized.len == confpath_len_wo_slash) + return mrb_str_new_lit(mrb, ""); + + assert(req->path_normalized.len > confpath_len_wo_slash); + + size_t path_info_start, path_info_end = req->query_at != SIZE_MAX ? req->query_at : req->path.len; + + if (req->norm_indexes == NULL) { + path_info_start = confpath_len_wo_slash; + } else if (req->norm_indexes[0] == 0 && confpath_len_wo_slash == 0) { + /* path without leading slash */ + path_info_start = 0; + } else { + path_info_start = req->norm_indexes[confpath_len_wo_slash] - 1; + } + + return mrb_str_new(mrb, req->path.base + path_info_start, path_info_end - path_info_start); +} + +static mrb_value build_env(h2o_mruby_generator_t *generator) +{ + h2o_mruby_shared_context_t *shared = generator->ctx->shared; + mrb_state *mrb = shared->mrb; + mrb_value env = mrb_hash_new_capa(mrb, 16); + char http_version[sizeof("HTTP/1.0")]; + size_t http_version_sz; + + /* environment */ + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_REQUEST_METHOD), + mrb_str_new(mrb, generator->req->method.base, generator->req->method.len)); + + size_t confpath_len_wo_slash = generator->req->pathconf->path.len; + if (generator->req->pathconf->path.base[generator->req->pathconf->path.len - 1] == '/') + --confpath_len_wo_slash; + assert(confpath_len_wo_slash <= generator->req->path_normalized.len); + + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_SCRIPT_NAME), + mrb_str_new(mrb, generator->req->pathconf->path.base, confpath_len_wo_slash)); + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_PATH_INFO), build_path_info(mrb, generator->req, confpath_len_wo_slash)); + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_QUERY_STRING), + generator->req->query_at != SIZE_MAX ? mrb_str_new(mrb, generator->req->path.base + generator->req->query_at + 1, + generator->req->path.len - (generator->req->query_at + 1)) + : mrb_str_new_lit(mrb, "")); + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_SERVER_NAME), + mrb_str_new(mrb, generator->req->hostconf->authority.host.base, generator->req->hostconf->authority.host.len)); + http_version_sz = h2o_stringify_protocol_version(http_version, generator->req->version); + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_SERVER_PROTOCOL), + mrb_str_new(mrb, http_version, http_version_sz)); + { + mrb_value h, p; + stringify_address(generator->req->conn, generator->req->conn->callbacks->get_sockname, mrb, &h, &p); + if (!mrb_nil_p(h)) + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_SERVER_ADDR), h); + if (!mrb_nil_p(p)) + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_SERVER_PORT), p); + } + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_TOKEN_HOST - h2o__tokens), + mrb_str_new(mrb, generator->req->authority.base, generator->req->authority.len)); + if (generator->req->entity.base != NULL) { + char buf[32]; + int l = sprintf(buf, "%zu", generator->req->entity.len); + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_CONTENT_LENGTH), mrb_str_new(mrb, buf, l)); + generator->rack_input = mrb_input_stream_value(mrb, NULL, 0); + mrb_input_stream_set_data(mrb, generator->rack_input, generator->req->entity.base, (mrb_int)generator->req->entity.len, 0, + on_rack_input_free, &generator->rack_input); + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_RACK_INPUT), generator->rack_input); + } + { + mrb_value h, p; + stringify_address(generator->req->conn, generator->req->conn->callbacks->get_peername, mrb, &h, &p); + if (!mrb_nil_p(h)) + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_REMOTE_ADDR), h); + if (!mrb_nil_p(p)) + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_REMOTE_PORT), p); + } + { + size_t i; + for (i = 0; i != generator->req->env.size; i += 2) { + h2o_iovec_t *name = generator->req->env.entries + i, *value = name + 1; + mrb_hash_set(mrb, env, mrb_str_new(mrb, name->base, name->len), mrb_str_new(mrb, value->base, value->len)); + } + } + + { /* headers */ + h2o_header_t **headers_sorted = alloca(sizeof(*headers_sorted) * generator->req->headers.size); + size_t i; + for (i = 0; i != generator->req->headers.size; ++i) + headers_sorted[i] = generator->req->headers.entries + i; + qsort(headers_sorted, generator->req->headers.size, sizeof(*headers_sorted), build_env_sort_header_cb); + for (i = 0; i != generator->req->headers.size; ++i) { + const h2o_header_t *header = headers_sorted[i]; + mrb_value n, v; + if (h2o_iovec_is_token(header->name)) { + const h2o_token_t *token = H2O_STRUCT_FROM_MEMBER(h2o_token_t, buf, header->name); + if (token == H2O_TOKEN_TRANSFER_ENCODING) + continue; + n = mrb_ary_entry(shared->constants, (mrb_int)(token - h2o__tokens)); + } else { + h2o_iovec_t vec = convert_header_name_to_env(&generator->req->pool, header->name->base, header->name->len); + n = mrb_str_new(mrb, vec.base, vec.len); + } + v = mrb_str_new(mrb, header->value.base, header->value.len); + while (i < generator->req->headers.size - 1) { + if (!h2o_memis(headers_sorted[i + 1]->name->base, headers_sorted[i + 1]->name->len, header->name->base, + header->name->len)) + break; + header = headers_sorted[++i]; + v = mrb_str_append(mrb, v, mrb_ary_entry(shared->constants, + header->name == &H2O_TOKEN_COOKIE->buf ? H2O_MRUBY_LIT_SEPARATOR_SEMICOLON + : H2O_MRUBY_LIT_SEPARATOR_COMMA)); + v = mrb_str_append(mrb, v, mrb_str_new(mrb, header->value.base, header->value.len)); + } + mrb_hash_set(mrb, env, n, v); + } + } + + /* rack.* */ + /* TBD rack.version? */ + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_RACK_URL_SCHEME), + mrb_str_new(mrb, generator->req->scheme->name.base, generator->req->scheme->name.len)); + /* we are using shared-none architecture, and therefore declare ourselves as multiprocess */ + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_RACK_MULTITHREAD), mrb_false_value()); + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_RACK_MULTIPROCESS), mrb_true_value()); + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_RACK_RUN_ONCE), mrb_false_value()); + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_RACK_HIJACK_), mrb_false_value()); + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_RACK_ERRORS), + mrb_gv_get(mrb, mrb_intern_lit(mrb, "$stderr"))); + + /* server name */ + mrb_hash_set(mrb, env, mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_SERVER_SOFTWARE), + mrb_ary_entry(shared->constants, H2O_MRUBY_LIT_SERVER_SOFTWARE_VALUE)); + + return env; +} + +static int handle_response_header(h2o_mruby_context_t *handler_ctx, h2o_iovec_t name, h2o_iovec_t value, void *_req) +{ + h2o_req_t *req = _req; + const h2o_token_t *token; + static const h2o_iovec_t fallthru_set_prefix = {H2O_STRLIT(FALLTHRU_SET_PREFIX)}; + + /* convert name to lowercase */ + name = h2o_strdup(&req->pool, name.base, name.len); + h2o_strtolower(name.base, name.len); + + if ((token = h2o_lookup_token(name.base, name.len)) != NULL) { + if (token->proxy_should_drop_for_res) { + /* skip */ + } else if (token == H2O_TOKEN_CONTENT_LENGTH) { + req->res.content_length = h2o_strtosize(value.base, value.len); + } else { + value = h2o_strdup(&req->pool, value.base, value.len); + if (token == H2O_TOKEN_LINK) { + h2o_iovec_t new_value = h2o_push_path_in_link_header(req, value.base, value.len); + if (new_value.len) + h2o_add_header(&req->pool, &req->res.headers, token, NULL, new_value.base, new_value.len); + } else { + h2o_add_header(&req->pool, &req->res.headers, token, NULL, value.base, value.len); + } + } + } else if (name.len > fallthru_set_prefix.len && + h2o_memis(name.base, fallthru_set_prefix.len, fallthru_set_prefix.base, fallthru_set_prefix.len)) { + /* register environment variables (with the name converted to uppercase, and using `_`) */ + size_t i; + name.base += fallthru_set_prefix.len; + name.len -= fallthru_set_prefix.len; + for (i = 0; i != name.len; ++i) + name.base[i] = name.base[i] == '-' ? '_' : h2o_toupper(name.base[i]); + h2o_iovec_t *slot = h2o_req_getenv(req, name.base, name.len, 1); + *slot = h2o_strdup(&req->pool, value.base, value.len); + } else { + value = h2o_strdup(&req->pool, value.base, value.len); + h2o_add_header_by_str(&req->pool, &req->res.headers, name.base, name.len, 0, NULL, value.base, value.len); + } + + return 0; +} + +static void clear_rack_input(h2o_mruby_generator_t *generator) +{ + if (!mrb_nil_p(generator->rack_input)) + mrb_input_stream_set_data(generator->ctx->shared->mrb, generator->rack_input, NULL, -1, 0, NULL, NULL); +} + +static void on_generator_dispose(void *_generator) +{ + h2o_mruby_generator_t *generator = _generator; + + clear_rack_input(generator); + generator->req = NULL; + + if (generator->chunked != NULL) + h2o_mruby_send_chunked_dispose(generator); +} + +static int on_req(h2o_handler_t *_handler, h2o_req_t *req) +{ + h2o_mruby_handler_t *handler = (void *)_handler; + h2o_mruby_shared_context_t *shared = get_shared_context(req->conn->ctx); + int gc_arena = mrb_gc_arena_save(shared->mrb); + + h2o_mruby_generator_t *generator = h2o_mem_alloc_shared(&req->pool, sizeof(*generator), on_generator_dispose); + generator->super.proceed = NULL; + generator->super.stop = NULL; + generator->req = req; + generator->ctx = h2o_context_get_handler_context(req->conn->ctx, &handler->super); + generator->rack_input = mrb_nil_value(); + generator->chunked = NULL; + + mrb_value env = build_env(generator); + + int is_delegate = 0; + h2o_mruby_run_fiber(generator, generator->ctx->proc, env, &is_delegate); + + mrb_gc_arena_restore(shared->mrb, gc_arena); + if (is_delegate) + return -1; + return 0; +} + +static void send_response(h2o_mruby_generator_t *generator, mrb_int status, mrb_value resp, int *is_delegate) +{ + mrb_state *mrb = generator->ctx->shared->mrb; + mrb_value body; + h2o_iovec_t content = {NULL}; + + /* set status */ + generator->req->res.status = (int)status; + + /* set headers */ + if (h2o_mruby_iterate_headers(generator->ctx, mrb_ary_entry(resp, 1), handle_response_header, generator->req) != 0) { + assert(mrb->exc != NULL); + goto GotException; + } + + /* return without processing body, if status is fallthru */ + if (generator->req->res.status == STATUS_FALLTHRU) { + if (is_delegate != NULL) + *is_delegate = 1; + else + h2o_delegate_request_deferred(generator->req, &generator->ctx->handler->super); + return; + } + + /* obtain body */ + body = mrb_ary_entry(resp, 2); + + /* flatten body if possible */ + if (mrb_array_p(body)) { + mrb_int i, len = RARRAY_LEN(body); + /* calculate the length of the output, while at the same time converting the elements of the output array to string */ + content.len = 0; + for (i = 0; i != len; ++i) { + mrb_value e = mrb_ary_entry(body, i); + if (!mrb_string_p(e)) { + e = h2o_mruby_to_str(mrb, e); + if (mrb->exc != NULL) + goto GotException; + mrb_ary_set(mrb, body, i, e); + } + content.len += RSTRING_LEN(e); + } + /* allocate memory, and copy the response */ + char *dst = content.base = h2o_mem_alloc_pool(&generator->req->pool, content.len); + for (i = 0; i != len; ++i) { + mrb_value e = mrb_ary_entry(body, i); + assert(mrb_string_p(e)); + memcpy(dst, RSTRING_PTR(e), RSTRING_LEN(e)); + dst += RSTRING_LEN(e); + } + /* reset body to nil, now that we have read all data */ + body = mrb_nil_value(); + } + + /* use fiber in case we need to call #each */ + if (!mrb_nil_p(body)) { + h2o_start_response(generator->req, &generator->super); + mrb_value receiver = h2o_mruby_send_chunked_init(generator, body); + if (!mrb_nil_p(receiver)) + h2o_mruby_run_fiber(generator, receiver, body, 0); + return; + } + + /* send the entire response immediately */ + if (status == 101 || status == 204 || status == 304 || + h2o_memis(generator->req->input.method.base, generator->req->input.method.len, H2O_STRLIT("HEAD"))) { + h2o_start_response(generator->req, &generator->super); + h2o_send(generator->req, NULL, 0, H2O_SEND_STATE_FINAL); + } else { + if (content.len < generator->req->res.content_length) { + generator->req->res.content_length = content.len; + } else { + content.len = generator->req->res.content_length; + } + h2o_start_response(generator->req, &generator->super); + h2o_send(generator->req, &content, 1, H2O_SEND_STATE_FINAL); + } + return; + +GotException: + report_exception(generator->req, mrb); + h2o_send_error_500(generator->req, "Internal Server Error", "Internal Server Error", 0); +} + +void h2o_mruby_run_fiber(h2o_mruby_generator_t *generator, mrb_value receiver, mrb_value input, int *is_delegate) +{ + mrb_state *mrb = generator->ctx->shared->mrb; + mrb_value output; + mrb_int status; + + if (!mrb_obj_eq(mrb, generator->ctx->proc, receiver)) { + mrb_gc_unregister(mrb, receiver); + mrb_gc_protect(mrb, receiver); + } + + h2o_mruby_current_generator = generator; + + while (1) { + /* send input to fiber */ + output = mrb_funcall_argv(mrb, receiver, generator->ctx->shared->symbols.sym_call, 1, &input); + if (mrb->exc != NULL) + goto GotException; + if (!mrb_array_p(output)) { + mrb->exc = mrb_obj_ptr(mrb_exc_new_str_lit(mrb, E_RUNTIME_ERROR, "rack app did not return an array")); + goto GotException; + } + /* fetch status */ + mrb_value v = mrb_to_int(mrb, mrb_ary_entry(output, 0)); + if (mrb->exc != NULL) + goto GotException; + status = mrb_fixnum(v); + /* take special action depending on the status code */ + if (status < 0) { + if (status == H2O_MRUBY_CALLBACK_ID_EXCEPTION_RAISED) { + mrb->exc = mrb_obj_ptr(mrb_ary_entry(output, 1)); + goto GotException; + } + receiver = mrb_ary_entry(output, 1); + int next_action = H2O_MRUBY_CALLBACK_NEXT_ACTION_IMMEDIATE; + mrb_value args = mrb_ary_entry(output, 2); + if (mrb_array_p(args)) { + switch (status) { + case H2O_MRUBY_CALLBACK_ID_SEND_CHUNKED_EOS: + input = h2o_mruby_send_chunked_eos_callback(generator, receiver, args, &next_action); + break; + case H2O_MRUBY_CALLBACK_ID_HTTP_JOIN_RESPONSE: + input = h2o_mruby_http_join_response_callback(generator, receiver, args, &next_action); + break; + case H2O_MRUBY_CALLBACK_ID_HTTP_FETCH_CHUNK: + input = h2o_mruby_http_fetch_chunk_callback(generator, receiver, args, &next_action); + break; + default: + input = mrb_exc_new_str_lit(mrb, E_RUNTIME_ERROR, "unexpected callback id sent from rack app"); + break; + } + } else { + input = mrb_exc_new_str_lit(mrb, E_RUNTIME_ERROR, "callback from rack app did not receive an array arg"); + } + switch (next_action) { + case H2O_MRUBY_CALLBACK_NEXT_ACTION_STOP: + return; + case H2O_MRUBY_CALLBACK_NEXT_ACTION_ASYNC: + goto Async; + default: + assert(next_action == H2O_MRUBY_CALLBACK_NEXT_ACTION_IMMEDIATE); + break; + } + goto Next; + } + /* if no special actions were necessary, then the output is a rack response */ + break; + Next: + mrb_gc_protect(mrb, receiver); + mrb_gc_protect(mrb, input); + } + + h2o_mruby_current_generator = NULL; + + if (!(100 <= status && status <= 999)) { + mrb->exc = mrb_obj_ptr(mrb_exc_new_str_lit(mrb, E_RUNTIME_ERROR, "status returned from rack app is out of range")); + goto GotException; + } + + /* send the response (unless req is already closed) */ + if (generator->req == NULL) + return; + if (generator->req->_generator != NULL) { + mrb->exc = mrb_obj_ptr(mrb_exc_new_str_lit(mrb, E_RUNTIME_ERROR, "unexpectedly received a rack response")); + goto GotException; + } + send_response(generator, status, output, is_delegate); + return; + +GotException: + h2o_mruby_current_generator = NULL; + if (generator->req != NULL) { + report_exception(generator->req, mrb); + if (generator->req->_generator == NULL) { + h2o_send_error_500(generator->req, "Internal Server Error", "Internal Server Error", 0); + } else { + h2o_mruby_send_chunked_close(generator); + } + } + return; + +Async: + h2o_mruby_current_generator = NULL; + if (!mrb_obj_eq(mrb, generator->ctx->proc, receiver)) + mrb_gc_register(mrb, receiver); + return; +} + +h2o_mruby_handler_t *h2o_mruby_register(h2o_pathconf_t *pathconf, h2o_mruby_config_vars_t *vars) +{ + h2o_mruby_handler_t *handler = (void *)h2o_create_handler(pathconf, sizeof(*handler)); + + handler->super.on_context_init = on_context_init; + handler->super.on_context_dispose = on_context_dispose; + handler->super.dispose = on_handler_dispose; + handler->super.on_req = on_req; + handler->config.source = h2o_strdup(NULL, vars->source.base, vars->source.len); + if (vars->path != NULL) + handler->config.path = h2o_strdup(NULL, vars->path, SIZE_MAX).base; + handler->config.lineno = vars->lineno; + + return handler; +} + +mrb_value h2o_mruby_each_to_array(h2o_mruby_context_t *handler_ctx, mrb_value src) +{ + return mrb_funcall_argv(handler_ctx->shared->mrb, mrb_ary_entry(handler_ctx->shared->constants, H2O_MRUBY_PROC_EACH_TO_ARRAY), + handler_ctx->shared->symbols.sym_call, 1, &src); +} + +static int iterate_headers_handle_pair(h2o_mruby_context_t *handler_ctx, mrb_value name, mrb_value value, + int (*cb)(h2o_mruby_context_t *, h2o_iovec_t, h2o_iovec_t, void *), void *cb_data) +{ + mrb_state *mrb = handler_ctx->shared->mrb; + + /* convert name and value to string */ + name = h2o_mruby_to_str(mrb, name); + if (mrb->exc != NULL) + return -1; + value = h2o_mruby_to_str(mrb, value); + if (mrb->exc != NULL) + return -1; + + /* call the callback, splitting the values with '\n' */ + const char *vstart = RSTRING_PTR(value), *vend = vstart + RSTRING_LEN(value), *eol; + while (1) { + for (eol = vstart; eol != vend; ++eol) + if (*eol == '\n') + break; + if (cb(handler_ctx, h2o_iovec_init(RSTRING_PTR(name), RSTRING_LEN(name)), h2o_iovec_init(vstart, eol - vstart), cb_data) != + 0) + return -1; + if (eol == vend) + break; + vstart = eol + 1; + } + + return 0; +} + +int h2o_mruby_iterate_headers(h2o_mruby_context_t *handler_ctx, mrb_value headers, + int (*cb)(h2o_mruby_context_t *, h2o_iovec_t, h2o_iovec_t, void *), void *cb_data) +{ + mrb_state *mrb = handler_ctx->shared->mrb; + + if (!(mrb_hash_p(headers) || mrb_array_p(headers))) { + headers = h2o_mruby_each_to_array(handler_ctx, headers); + if (mrb->exc != NULL) + return -1; + assert(mrb_array_p(headers)); + } + + if (mrb_hash_p(headers)) { + mrb_value keys = mrb_hash_keys(mrb, headers); + mrb_int i, len = RARRAY_LEN(keys); + for (i = 0; i != len; ++i) { + mrb_value k = mrb_ary_entry(keys, i); + mrb_value v = mrb_hash_get(mrb, headers, k); + if (iterate_headers_handle_pair(handler_ctx, k, v, cb, cb_data) != 0) + return -1; + } + } else { + assert(mrb_array_p(headers)); + mrb_int i, len = RARRAY_LEN(headers); + for (i = 0; i != len; ++i) { + mrb_value pair = mrb_ary_entry(headers, i); + if (!mrb_array_p(pair)) { + mrb->exc = mrb_obj_ptr(mrb_exc_new_str_lit(mrb, E_ARGUMENT_ERROR, "array element of headers MUST by an array")); + return -1; + } + if (iterate_headers_handle_pair(handler_ctx, mrb_ary_entry(pair, 0), mrb_ary_entry(pair, 1), cb, cb_data) != 0) + return -1; + } + } + + return 0; +} diff --git a/web/server/h2o/libh2o/lib/handler/mruby/chunked.c b/web/server/h2o/libh2o/lib/handler/mruby/chunked.c new file mode 100644 index 000000000..28e3ae433 --- /dev/null +++ b/web/server/h2o/libh2o/lib/handler/mruby/chunked.c @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2015-2016 DeNA Co., Ltd., Kazuho Oku + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +#include <stdlib.h> +#include <mruby.h> +#include <mruby/array.h> +#include <mruby/error.h> +#include <mruby/string.h> +#include "h2o/mruby_.h" +#include "embedded.c.h" + +struct st_h2o_mruby_chunked_t { + h2o_doublebuffer_t sending; + size_t bytes_left; /* SIZE_MAX indicates that the number is undermined */ + enum { H2O_MRUBY_CHUNKED_TYPE_CALLBACK, H2O_MRUBY_CHUNKED_TYPE_SHORTCUT } type; + union { + struct { + h2o_buffer_t *receiving; + mrb_value body_obj; /* becomes nil on eos */ + } callback; + struct { + h2o_mruby_http_request_context_t *client; + h2o_buffer_t *remaining; + } shortcut; + }; +}; + +static void do_send(h2o_mruby_generator_t *generator, h2o_buffer_t **input, int is_final) +{ + h2o_mruby_chunked_t *chunked = generator->chunked; + + assert(chunked->sending.bytes_inflight == 0); + + h2o_iovec_t buf = h2o_doublebuffer_prepare(&chunked->sending, input, generator->req->preferred_chunk_size); + size_t bufcnt = 1; + + if (is_final && buf.len == chunked->sending.buf->size && (*input)->size == 0) { + if (buf.len == 0) + --bufcnt; + /* terminate the H1 connection if the length of content served did not match the value sent in content-length header */ + if (chunked->bytes_left != SIZE_MAX && chunked->bytes_left != 0) + generator->req->http1_is_persistent = 0; + } else { + if (buf.len == 0) + return; + is_final = 0; + } + + h2o_send(generator->req, &buf, bufcnt, is_final ? H2O_SEND_STATE_FINAL : H2O_SEND_STATE_IN_PROGRESS); +} + +static void do_proceed(h2o_generator_t *_generator, h2o_req_t *req) +{ + h2o_mruby_generator_t *generator = (void *)_generator; + h2o_mruby_chunked_t *chunked = generator->chunked; + h2o_buffer_t **input; + int is_final; + + h2o_doublebuffer_consume(&chunked->sending); + + switch (chunked->type) { + case H2O_MRUBY_CHUNKED_TYPE_CALLBACK: + input = &chunked->callback.receiving; + is_final = mrb_nil_p(chunked->callback.body_obj); + break; + case H2O_MRUBY_CHUNKED_TYPE_SHORTCUT: + if (chunked->shortcut.client != NULL) { + input = h2o_mruby_http_peek_content(chunked->shortcut.client, &is_final); + assert(!is_final); + } else { + input = &chunked->shortcut.remaining; + is_final = 1; + } + break; + default: + h2o_fatal("unexpected type"); + break; + } + + do_send(generator, input, is_final); +} + +static void on_shortcut_notify(h2o_mruby_generator_t *generator) +{ + h2o_mruby_chunked_t *chunked = generator->chunked; + int is_final; + h2o_buffer_t **input = h2o_mruby_http_peek_content(chunked->shortcut.client, &is_final); + + if (chunked->bytes_left != SIZE_MAX) { + if (chunked->bytes_left < (*input)->size) + (*input)->size = chunked->bytes_left; /* trim data too long */ + chunked->bytes_left -= (*input)->size; + } + + /* if final, steal socket input buffer to shortcut.remaining, and reset pointer to client */ + if (is_final) { + chunked->shortcut.remaining = *input; + h2o_buffer_init(input, &h2o_socket_buffer_prototype); + input = &chunked->shortcut.remaining; + chunked->shortcut.client = NULL; + } + + if (chunked->sending.bytes_inflight == 0) + do_send(generator, input, is_final); +} + +static void close_body_obj(h2o_mruby_generator_t *generator) +{ + h2o_mruby_chunked_t *chunked = generator->chunked; + mrb_state *mrb = generator->ctx->shared->mrb; + + if (!mrb_nil_p(chunked->callback.body_obj)) { + /* call close and throw away error */ + if (mrb_respond_to(mrb, chunked->callback.body_obj, generator->ctx->shared->symbols.sym_close)) + mrb_funcall_argv(mrb, chunked->callback.body_obj, generator->ctx->shared->symbols.sym_close, 0, NULL); + mrb->exc = NULL; + mrb_gc_unregister(mrb, chunked->callback.body_obj); + chunked->callback.body_obj = mrb_nil_value(); + } +} + +mrb_value h2o_mruby_send_chunked_init(h2o_mruby_generator_t *generator, mrb_value body) +{ + h2o_mruby_chunked_t *chunked = h2o_mem_alloc_pool(&generator->req->pool, sizeof(*chunked)); + h2o_doublebuffer_init(&chunked->sending, &h2o_socket_buffer_prototype); + chunked->bytes_left = h2o_memis(generator->req->method.base, generator->req->method.len, H2O_STRLIT("HEAD")) + ? 0 + : generator->req->res.content_length; + generator->super.proceed = do_proceed; + generator->chunked = chunked; + + if ((chunked->shortcut.client = h2o_mruby_http_set_shortcut(generator->ctx->shared->mrb, body, on_shortcut_notify)) != NULL) { + chunked->type = H2O_MRUBY_CHUNKED_TYPE_SHORTCUT; + chunked->shortcut.remaining = NULL; + on_shortcut_notify(generator); + return mrb_nil_value(); + } else { + chunked->type = H2O_MRUBY_CHUNKED_TYPE_CALLBACK; + h2o_buffer_init(&chunked->callback.receiving, &h2o_socket_buffer_prototype); + mrb_gc_register(generator->ctx->shared->mrb, body); + chunked->callback.body_obj = body; + return mrb_ary_entry(generator->ctx->shared->constants, H2O_MRUBY_CHUNKED_PROC_EACH_TO_FIBER); + } +} + +void h2o_mruby_send_chunked_dispose(h2o_mruby_generator_t *generator) +{ + h2o_mruby_chunked_t *chunked = generator->chunked; + + h2o_doublebuffer_dispose(&chunked->sending); + + switch (chunked->type) { + case H2O_MRUBY_CHUNKED_TYPE_CALLBACK: + h2o_buffer_dispose(&chunked->callback.receiving); + close_body_obj(generator); + break; + case H2O_MRUBY_CHUNKED_TYPE_SHORTCUT: + /* note: no need to free reference from chunked->client, since it is disposed at the same moment */ + if (chunked->shortcut.remaining != NULL) + h2o_buffer_dispose(&chunked->shortcut.remaining); + break; + } +} + +static mrb_value check_precond(mrb_state *mrb, h2o_mruby_generator_t *generator) +{ + if (generator == NULL || generator->req == NULL) + return mrb_exc_new_str_lit(mrb, E_RUNTIME_ERROR, "downstream HTTP closed"); + if (generator->req->_generator == NULL) + return mrb_exc_new_str_lit(mrb, E_RUNTIME_ERROR, "cannot send chunk before sending headers"); + return mrb_nil_value(); +} + +static mrb_value send_chunked_method(mrb_state *mrb, mrb_value self) +{ + h2o_mruby_generator_t *generator = h2o_mruby_current_generator; + const char *s; + mrb_int len; + + /* parse args */ + mrb_get_args(mrb, "s", &s, &len); + + { /* precond check */ + mrb_value exc = check_precond(mrb, generator); + if (!mrb_nil_p(exc)) + mrb_exc_raise(mrb, exc); + } + + /* append to send buffer, and send out immediately if necessary */ + if (len != 0) { + h2o_mruby_chunked_t *chunked = generator->chunked; + if (chunked->bytes_left != SIZE_MAX) { + if (len > chunked->bytes_left) + len = chunked->bytes_left; + chunked->bytes_left -= len; + } + if (len != 0) { + h2o_buffer_reserve(&chunked->callback.receiving, len); + memcpy(chunked->callback.receiving->bytes + chunked->callback.receiving->size, s, len); + chunked->callback.receiving->size += len; + if (chunked->sending.bytes_inflight == 0) + do_send(generator, &chunked->callback.receiving, 0); + } + } + + return mrb_nil_value(); +} + +mrb_value h2o_mruby_send_chunked_eos_callback(h2o_mruby_generator_t *generator, mrb_value receiver, mrb_value input, + int *next_action) +{ + mrb_state *mrb = generator->ctx->shared->mrb; + + { /* precond check */ + mrb_value exc = check_precond(mrb, generator); + if (!mrb_nil_p(exc)) + return exc; + } + + h2o_mruby_send_chunked_close(generator); + + *next_action = H2O_MRUBY_CALLBACK_NEXT_ACTION_STOP; + return mrb_nil_value(); +} + +void h2o_mruby_send_chunked_close(h2o_mruby_generator_t *generator) +{ + h2o_mruby_chunked_t *chunked = generator->chunked; + + /* run_fiber will never be called once we enter the fast path, and therefore this function will never get called in that case */ + assert(chunked->type == H2O_MRUBY_CHUNKED_TYPE_CALLBACK); + + close_body_obj(generator); + + if (chunked->sending.bytes_inflight == 0) + do_send(generator, &chunked->callback.receiving, 1); +} + +void h2o_mruby_send_chunked_init_context(h2o_mruby_shared_context_t *shared_ctx) +{ + mrb_state *mrb = shared_ctx->mrb; + + h2o_mruby_eval_expr(mrb, H2O_MRUBY_CODE_CHUNKED); + h2o_mruby_assert(mrb); + + mrb_define_method(mrb, mrb->kernel_module, "_h2o_send_chunk", send_chunked_method, MRB_ARGS_ARG(1, 0)); + h2o_mruby_define_callback(mrb, "_h2o_send_chunk_eos", H2O_MRUBY_CALLBACK_ID_SEND_CHUNKED_EOS); + + mrb_ary_set(mrb, shared_ctx->constants, H2O_MRUBY_CHUNKED_PROC_EACH_TO_FIBER, + mrb_funcall(mrb, mrb_top_self(mrb), "_h2o_chunked_proc_each_to_fiber", 0)); + h2o_mruby_assert(mrb); +} diff --git a/web/server/h2o/libh2o/lib/handler/mruby/embedded.c.h b/web/server/h2o/libh2o/lib/handler/mruby/embedded.c.h new file mode 100644 index 000000000..db4bb2321 --- /dev/null +++ b/web/server/h2o/libh2o/lib/handler/mruby/embedded.c.h @@ -0,0 +1,111 @@ +/* + * DO NOT EDIT! generated by embed_mruby_code.pl + * Please refer to the respective source files for copyright information. + */ + +/* lib/handler/mruby/embedded/core.rb */ +#define H2O_MRUBY_CODE_CORE \ + "module Kernel\n" \ + " def _h2o_define_callback(name, id)\n" \ + " Kernel.define_method(name) do |*args|\n" \ + " ret = Fiber.yield([ id, _h2o_create_resumer(), args ])\n" \ + " if ret.kind_of? Exception\n" \ + " raise ret\n" \ + " end\n" \ + " ret\n" \ + " end\n" \ + " end\n" \ + " def _h2o_create_resumer()\n" \ + " me = Fiber.current\n" \ + " Proc.new do |v|\n" \ + " me.resume(v)\n" \ + " end\n" \ + " end\n" \ + " def _h2o_proc_each_to_array()\n" \ + " Proc.new do |o|\n" \ + " a = []\n" \ + " o.each do |x|\n" \ + " a << x\n" \ + " end\n" \ + " a\n" \ + " end\n" \ + " end\n" \ + " def _h2o_proc_app_to_fiber()\n" \ + " Proc.new do |app|\n" \ + " cached = nil\n" \ + " Proc.new do |req|\n" \ + " fiber = cached\n" \ + " cached = nil\n" \ + " if !fiber\n" \ + " fiber = Fiber.new do\n" \ + " self_fiber = Fiber.current\n" \ + " req = Fiber.yield\n" \ + " while 1\n" \ + " begin\n" \ + " while 1\n" \ + " resp = app.call(req)\n" \ + " cached = self_fiber\n" \ + " req = Fiber.yield(resp)\n" \ + " end\n" \ + " rescue => e\n" \ + " cached = self_fiber\n" \ + " req = Fiber.yield([-1, e])\n" \ + " end\n" \ + " end\n" \ + " end\n" \ + " fiber.resume\n" \ + " end\n" \ + " fiber.resume(req)\n" \ + " end\n" \ + " end\n" \ + " end\n" \ + "end\n" + +/* lib/handler/mruby/embedded/http_request.rb */ +#define H2O_MRUBY_CODE_HTTP_REQUEST \ + "module H2O\n" \ + " class HttpRequest\n" \ + " def join\n" \ + " if !@resp\n" \ + " @resp = _h2o__http_join_response(self)\n" \ + " end\n" \ + " @resp\n" \ + " end\n" \ + " def _set_response(resp)\n" \ + " @resp = resp\n" \ + " end\n" \ + " end\n" \ + " class HttpInputStream\n" \ + " def each\n" \ + " while c = _h2o__http_fetch_chunk(self)\n" \ + " yield c\n" \ + " end\n" \ + " end\n" \ + " def join\n" \ + " s = \"\"\n" \ + " each do |c|\n" \ + " s << c\n" \ + " end\n" \ + " s\n" \ + " end\n" \ + " class Empty < HttpInputStream\n" \ + " def each; end\n" \ + " end\n" \ + " end\n" \ + "end\n" + +/* lib/handler/mruby/embedded/chunked.rb */ +#define H2O_MRUBY_CODE_CHUNKED \ + "module Kernel\n" \ + " def _h2o_chunked_proc_each_to_fiber()\n" \ + " Proc.new do |src|\n" \ + " fiber = Fiber.new do\n" \ + " src.each do |chunk|\n" \ + " _h2o_send_chunk(chunk)\n" \ + " end\n" \ + " _h2o_send_chunk_eos()\n" \ + " end\n" \ + " fiber.resume\n" \ + " end\n" \ + " end\n" \ + "end\n" diff --git a/web/server/h2o/libh2o/lib/handler/mruby/embedded/chunked.rb b/web/server/h2o/libh2o/lib/handler/mruby/embedded/chunked.rb new file mode 100644 index 000000000..ff4e578f8 --- /dev/null +++ b/web/server/h2o/libh2o/lib/handler/mruby/embedded/chunked.rb @@ -0,0 +1,35 @@ +# Copyright (c) 2014 DeNA Co., Ltd. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +module Kernel + + def _h2o_chunked_proc_each_to_fiber() + Proc.new do |src| + fiber = Fiber.new do + src.each do |chunk| + _h2o_send_chunk(chunk) + end + _h2o_send_chunk_eos() + end + fiber.resume + end + end + +end diff --git a/web/server/h2o/libh2o/lib/handler/mruby/embedded/core.rb b/web/server/h2o/libh2o/lib/handler/mruby/embedded/core.rb new file mode 100644 index 000000000..e62583df4 --- /dev/null +++ b/web/server/h2o/libh2o/lib/handler/mruby/embedded/core.rb @@ -0,0 +1,81 @@ +# Copyright (c) 2014-2016 DeNA Co., Ltd., Kazuho Oku, Ryosuke Matsumoto, +# Masayoshi Takahashi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +module Kernel + + def _h2o_define_callback(name, id) + Kernel.define_method(name) do |*args| + ret = Fiber.yield([ id, _h2o_create_resumer(), args ]) + if ret.kind_of? Exception + raise ret + end + ret + end + end + + def _h2o_create_resumer() + me = Fiber.current + Proc.new do |v| + me.resume(v) + end + end + + def _h2o_proc_each_to_array() + Proc.new do |o| + a = [] + o.each do |x| + a << x + end + a + end + end + + def _h2o_proc_app_to_fiber() + Proc.new do |app| + cached = nil + Proc.new do |req| + fiber = cached + cached = nil + if !fiber + fiber = Fiber.new do + self_fiber = Fiber.current + req = Fiber.yield + while 1 + begin + while 1 + resp = app.call(req) + cached = self_fiber + req = Fiber.yield(resp) + end + rescue => e + cached = self_fiber + req = Fiber.yield([-1, e]) + end + end + end + fiber.resume + end + fiber.resume(req) + end + end + end + +end diff --git a/web/server/h2o/libh2o/lib/handler/mruby/embedded/http_request.rb b/web/server/h2o/libh2o/lib/handler/mruby/embedded/http_request.rb new file mode 100644 index 000000000..3f3247a3c --- /dev/null +++ b/web/server/h2o/libh2o/lib/handler/mruby/embedded/http_request.rb @@ -0,0 +1,54 @@ +# Copyright (c) 2015-2016 DeNA Co., Ltd., Kazuho Oku +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +module H2O + + class HttpRequest + def join + if !@resp + @resp = _h2o__http_join_response(self) + end + @resp + end + def _set_response(resp) + @resp = resp + end + end + + class HttpInputStream + def each + while c = _h2o__http_fetch_chunk(self) + yield c + end + end + def join + s = "" + each do |c| + s << c + end + s + end + + class Empty < HttpInputStream + def each; end + end + end + +end diff --git a/web/server/h2o/libh2o/lib/handler/mruby/http_request.c b/web/server/h2o/libh2o/lib/handler/mruby/http_request.c new file mode 100644 index 000000000..964d03f19 --- /dev/null +++ b/web/server/h2o/libh2o/lib/handler/mruby/http_request.c @@ -0,0 +1,500 @@ +/* + * Copyright (c) 2015-2016 DeNA Co., Ltd., Kazuho Oku + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +#include <mruby.h> +#include <mruby/array.h> +#include <mruby/error.h> +#include <mruby/hash.h> +#include <mruby/string.h> +#include <mruby_input_stream.h> +#include "h2o/mruby_.h" +#include "embedded.c.h" + +struct st_h2o_mruby_http_request_context_t { + h2o_mruby_generator_t *generator; + h2o_http1client_t *client; + mrb_value receiver; + struct { + h2o_buffer_t *buf; + h2o_iovec_t body; /* body.base != NULL indicates that post content exists (and the length MAY be zero) */ + unsigned method_is_head : 1; + unsigned has_transfer_encoding : 1; + } req; + struct { + h2o_buffer_t *after_closed; /* when client becomes NULL, rest of the data will be stored to this pointer */ + int has_content; + } resp; + struct { + mrb_value request; + mrb_value input_stream; + } refs; + void (*shortcut_notify_cb)(h2o_mruby_generator_t *generator); +}; + +static void on_gc_dispose_request(mrb_state *mrb, void *_ctx) +{ + struct st_h2o_mruby_http_request_context_t *ctx = _ctx; + if (ctx != NULL) + ctx->refs.request = mrb_nil_value(); +} + +const static struct mrb_data_type request_type = {"http_request", on_gc_dispose_request}; + +static void on_gc_dispose_input_stream(mrb_state *mrb, void *_ctx) +{ + struct st_h2o_mruby_http_request_context_t *ctx = _ctx; + if (ctx != NULL) + ctx->refs.input_stream = mrb_nil_value(); +} + +const static struct mrb_data_type input_stream_type = {"http_input_stream", on_gc_dispose_input_stream}; + +static mrb_value create_downstream_closed_exception(mrb_state *mrb) +{ + return mrb_exc_new_str_lit(mrb, E_RUNTIME_ERROR, "downstream HTTP closed"); +} + +static mrb_value detach_receiver(struct st_h2o_mruby_http_request_context_t *ctx) +{ + mrb_value ret = ctx->receiver; + assert(!mrb_nil_p(ret)); + ctx->receiver = mrb_nil_value(); + return ret; +} + +static void on_dispose(void *_ctx) +{ + struct st_h2o_mruby_http_request_context_t *ctx = _ctx; + + /* clear the refs */ + if (ctx->client != NULL) { + h2o_http1client_cancel(ctx->client); + ctx->client = NULL; + } + if (!mrb_nil_p(ctx->refs.request)) + DATA_PTR(ctx->refs.request) = NULL; + if (!mrb_nil_p(ctx->refs.input_stream)) + DATA_PTR(ctx->refs.input_stream) = NULL; + + /* clear bufs */ + h2o_buffer_dispose(&ctx->req.buf); + h2o_buffer_dispose(&ctx->resp.after_closed); + + /* notify the app, if it is waiting to hear from us */ + if (!mrb_nil_p(ctx->receiver)) { + mrb_state *mrb = ctx->generator->ctx->shared->mrb; + int gc_arena = mrb_gc_arena_save(mrb); + h2o_mruby_run_fiber(ctx->generator, detach_receiver(ctx), create_downstream_closed_exception(mrb), NULL); + mrb_gc_arena_restore(mrb, gc_arena); + } +} + +static void post_response(struct st_h2o_mruby_http_request_context_t *ctx, int status, const h2o_header_t *headers_sorted, + size_t num_headers) +{ + mrb_state *mrb = ctx->generator->ctx->shared->mrb; + int gc_arena = mrb_gc_arena_save(mrb); + size_t i; + + mrb_value resp = mrb_ary_new_capa(mrb, 3); + + /* set status */ + mrb_ary_set(mrb, resp, 0, mrb_fixnum_value(status)); + + /* set headers */ + mrb_value headers_hash = mrb_hash_new_capa(mrb, (int)num_headers); + for (i = 0; i < num_headers; ++i) { + /* skip the headers, we determine the eos! */ + if (h2o_memis(headers_sorted[i].name, headers_sorted[i].name->len, H2O_STRLIT("content-length")) || + h2o_memis(headers_sorted[i].name, headers_sorted[i].name->len, H2O_STRLIT("transfer-encoding"))) + continue; + /* build and set the hash entry */ + mrb_value k = mrb_str_new(mrb, headers_sorted[i].name->base, headers_sorted[i].name->len); + mrb_value v = mrb_str_new(mrb, headers_sorted[i].value.base, headers_sorted[i].value.len); + while (i + 1 < num_headers && h2o_memis(headers_sorted[i].name->base, headers_sorted[i].name->len, + headers_sorted[i + 1].name->base, headers_sorted[i + 1].name->len)) { + ++i; + v = mrb_str_cat_lit(mrb, v, "\n"); + v = mrb_str_cat(mrb, v, headers_sorted[i].value.base, headers_sorted[i].value.len); + } + mrb_hash_set(mrb, headers_hash, k, v); + } + mrb_ary_set(mrb, resp, 1, headers_hash); + + /* set input stream */ + assert(mrb_nil_p(ctx->refs.input_stream)); + mrb_value input_stream_class; + if (ctx->req.method_is_head || status == 101 || status == 204 || status == 304) { + input_stream_class = mrb_ary_entry(ctx->generator->ctx->shared->constants, H2O_MRUBY_HTTP_EMPTY_INPUT_STREAM_CLASS); + } else { + input_stream_class = mrb_ary_entry(ctx->generator->ctx->shared->constants, H2O_MRUBY_HTTP_INPUT_STREAM_CLASS); + } + ctx->refs.input_stream = h2o_mruby_create_data_instance(mrb, input_stream_class, ctx, &input_stream_type); + mrb_ary_set(mrb, resp, 2, ctx->refs.input_stream); + + if (mrb_nil_p(ctx->receiver)) { + /* is async */ + mrb_funcall(mrb, ctx->refs.request, "_set_response", 1, resp); + if (mrb->exc != NULL) { + fprintf(stderr, "_set_response failed\n"); + abort(); + } + } else { + /* send response to the waiting receiver */ + h2o_mruby_run_fiber(ctx->generator, detach_receiver(ctx), resp, NULL); + } + + mrb_gc_arena_restore(mrb, gc_arena); +} + +static void post_error(struct st_h2o_mruby_http_request_context_t *ctx, const char *errstr) +{ + static const h2o_header_t headers_sorted[] = { + {&H2O_TOKEN_CONTENT_TYPE->buf, NULL, {H2O_STRLIT("text/plain; charset=utf-8")}}, + }; + + ctx->client = NULL; + size_t errstr_len = strlen(errstr); + h2o_buffer_reserve(&ctx->resp.after_closed, errstr_len); + memcpy(ctx->resp.after_closed->bytes + ctx->resp.after_closed->size, errstr, errstr_len); + ctx->resp.after_closed->size += errstr_len; + ctx->resp.has_content = 1; + + post_response(ctx, 500, headers_sorted, sizeof(headers_sorted) / sizeof(headers_sorted[0])); +} + +static mrb_value build_chunk(struct st_h2o_mruby_http_request_context_t *ctx) +{ + mrb_value chunk; + + assert(ctx->resp.has_content); + + if (ctx->client != NULL) { + assert(ctx->client->sock->input->size != 0); + chunk = mrb_str_new(ctx->generator->ctx->shared->mrb, ctx->client->sock->input->bytes, ctx->client->sock->input->size); + h2o_buffer_consume(&ctx->client->sock->input, ctx->client->sock->input->size); + ctx->resp.has_content = 0; + } else { + if (ctx->resp.after_closed->size == 0) { + chunk = mrb_nil_value(); + } else { + chunk = mrb_str_new(ctx->generator->ctx->shared->mrb, ctx->resp.after_closed->bytes, ctx->resp.after_closed->size); + h2o_buffer_consume(&ctx->resp.after_closed, ctx->resp.after_closed->size); + } + /* has_content is retained as true, so that repeated calls will return nil immediately */ + } + + return chunk; +} + +static int on_body(h2o_http1client_t *client, const char *errstr) +{ + struct st_h2o_mruby_http_request_context_t *ctx = client->data; + + if (errstr != NULL) { + h2o_buffer_t *tmp = ctx->resp.after_closed; + ctx->resp.after_closed = client->sock->input; + client->sock->input = tmp; + ctx->client = NULL; + ctx->resp.has_content = 1; + } else if (client->sock->input->size != 0) { + ctx->resp.has_content = 1; + } + + if (ctx->resp.has_content) { + if (ctx->shortcut_notify_cb != NULL) { + ctx->shortcut_notify_cb(ctx->generator); + } else if (!mrb_nil_p(ctx->receiver)) { + int gc_arena = mrb_gc_arena_save(ctx->generator->ctx->shared->mrb); + mrb_value chunk = build_chunk(ctx); + h2o_mruby_run_fiber(ctx->generator, detach_receiver(ctx), chunk, NULL); + mrb_gc_arena_restore(ctx->generator->ctx->shared->mrb, gc_arena); + } + } + return 0; +} + +static int headers_sort_cb(const void *_x, const void *_y) +{ + const h2o_header_t *x = _x, *y = _y; + + if (x->name->len < y->name->len) + return -1; + if (x->name->len > y->name->len) + return 1; + return memcmp(x->name->base, y->name->base, x->name->len); +} + +static h2o_http1client_body_cb on_head(h2o_http1client_t *client, const char *errstr, int minor_version, int status, + h2o_iovec_t msg, h2o_header_t *headers, size_t num_headers, int rlen) +{ + struct st_h2o_mruby_http_request_context_t *ctx = client->data; + + if (errstr != NULL) { + if (errstr != h2o_http1client_error_is_eos) { + /* error */ + post_error(ctx, errstr); + return NULL; + } + /* closed without body */ + ctx->client = NULL; + } + + qsort(headers, num_headers, sizeof(headers[0]), headers_sort_cb); + post_response(ctx, status, headers, num_headers); + return on_body; +} + +static h2o_http1client_head_cb on_connect(h2o_http1client_t *client, const char *errstr, h2o_iovec_t **reqbufs, size_t *reqbufcnt, + int *method_is_head) +{ + struct st_h2o_mruby_http_request_context_t *ctx = client->data; + + if (errstr != NULL) { + post_error(ctx, errstr); + return NULL; + } + + *reqbufs = h2o_mem_alloc_pool(&ctx->generator->req->pool, sizeof(**reqbufs) * 2); + **reqbufs = h2o_iovec_init(ctx->req.buf->bytes, ctx->req.buf->size); + *reqbufcnt = 1; + if (ctx->req.body.base != NULL) + (*reqbufs)[(*reqbufcnt)++] = ctx->req.body; + *method_is_head = ctx->req.method_is_head; + return on_head; +} + +static inline void append_to_buffer(h2o_buffer_t **buf, const void *src, size_t len) +{ + memcpy((*buf)->bytes + (*buf)->size, src, len); + (*buf)->size += len; +} + +static int flatten_request_header(h2o_mruby_context_t *handler_ctx, h2o_iovec_t name, h2o_iovec_t value, void *_ctx) +{ + struct st_h2o_mruby_http_request_context_t *ctx = _ctx; + + /* ignore certain headers */ + if (h2o_lcstris(name.base, name.len, H2O_STRLIT("content-length")) || + h2o_lcstris(name.base, name.len, H2O_STRLIT("connection")) || h2o_lcstris(name.base, name.len, H2O_STRLIT("host"))) + return 0; + + /* mark the existence of transfer-encoding in order to prevent us from adding content-length header */ + if (h2o_lcstris(name.base, name.len, H2O_STRLIT("transfer-encoding"))) + ctx->req.has_transfer_encoding = 1; + + h2o_buffer_reserve(&ctx->req.buf, name.len + value.len + sizeof(": \r\n") - 1); + append_to_buffer(&ctx->req.buf, name.base, name.len); + append_to_buffer(&ctx->req.buf, H2O_STRLIT(": ")); + append_to_buffer(&ctx->req.buf, value.base, value.len); + append_to_buffer(&ctx->req.buf, H2O_STRLIT("\r\n")); + + return 0; +} + +static mrb_value http_request_method(mrb_state *mrb, mrb_value self) +{ + h2o_mruby_generator_t *generator; + struct st_h2o_mruby_http_request_context_t *ctx; + const char *arg_url; + mrb_int arg_url_len; + mrb_value arg_hash; + h2o_iovec_t method; + h2o_url_t url; + + /* parse args */ + arg_hash = mrb_nil_value(); + mrb_get_args(mrb, "s|H", &arg_url, &arg_url_len, &arg_hash); + + /* precond check */ + if ((generator = h2o_mruby_current_generator) == NULL || generator->req == NULL) + mrb_exc_raise(mrb, create_downstream_closed_exception(mrb)); + + /* allocate context and initialize */ + ctx = h2o_mem_alloc_shared(&generator->req->pool, sizeof(*ctx), on_dispose); + memset(ctx, 0, sizeof(*ctx)); + ctx->generator = generator; + ctx->receiver = mrb_nil_value(); + h2o_buffer_init(&ctx->req.buf, &h2o_socket_buffer_prototype); + h2o_buffer_init(&ctx->resp.after_closed, &h2o_socket_buffer_prototype); + ctx->refs.request = mrb_nil_value(); + ctx->refs.input_stream = mrb_nil_value(); + + /* uri */ + if (h2o_url_parse(arg_url, arg_url_len, &url) != 0) + mrb_raise(mrb, E_ARGUMENT_ERROR, "invaild URL"); + + /* method */ + method = h2o_iovec_init(H2O_STRLIT("GET")); + if (mrb_hash_p(arg_hash)) { + mrb_value t = mrb_hash_get(mrb, arg_hash, mrb_symbol_value(generator->ctx->shared->symbols.sym_method)); + if (!mrb_nil_p(t)) { + t = mrb_str_to_str(mrb, t); + method = h2o_iovec_init(RSTRING_PTR(t), RSTRING_LEN(t)); + if (h2o_memis(method.base, method.len, H2O_STRLIT("HEAD"))) { + ctx->req.method_is_head = 1; + } + } + } + + /* start building the request */ + h2o_buffer_reserve(&ctx->req.buf, method.len + 1); + append_to_buffer(&ctx->req.buf, method.base, method.len); + append_to_buffer(&ctx->req.buf, H2O_STRLIT(" ")); + h2o_buffer_reserve(&ctx->req.buf, + url.path.len + url.authority.len + sizeof(" HTTP/1.1\r\nConnection: close\r\nHost: \r\n") - 1); + append_to_buffer(&ctx->req.buf, url.path.base, url.path.len); + append_to_buffer(&ctx->req.buf, H2O_STRLIT(" HTTP/1.1\r\nConnection: close\r\nHost: ")); + append_to_buffer(&ctx->req.buf, url.authority.base, url.authority.len); + append_to_buffer(&ctx->req.buf, H2O_STRLIT("\r\n")); + + /* headers */ + if (mrb_hash_p(arg_hash)) { + mrb_value headers = mrb_hash_get(mrb, arg_hash, mrb_symbol_value(generator->ctx->shared->symbols.sym_headers)); + if (!mrb_nil_p(headers)) { + if (h2o_mruby_iterate_headers(generator->ctx, headers, flatten_request_header, ctx) != 0) { + mrb_value exc = mrb_obj_value(mrb->exc); + mrb->exc = NULL; + mrb_exc_raise(mrb, exc); + } + } + } + /* body */ + if (mrb_hash_p(arg_hash)) { + mrb_value body = mrb_hash_get(mrb, arg_hash, mrb_symbol_value(generator->ctx->shared->symbols.sym_body)); + if (!mrb_nil_p(body)) { + if (mrb_obj_eq(mrb, body, generator->rack_input)) { + /* fast path */ + mrb_int pos; + mrb_input_stream_get_data(mrb, body, NULL, NULL, &pos, NULL, NULL); + ctx->req.body = generator->req->entity; + ctx->req.body.base += pos; + ctx->req.body.len -= pos; + } else { + if (!mrb_string_p(body)) { + body = mrb_funcall(mrb, body, "read", 0); + if (!mrb_string_p(body)) + mrb_raise(mrb, E_ARGUMENT_ERROR, "body.read did not return string"); + } + ctx->req.body = h2o_strdup(&ctx->generator->req->pool, RSTRING_PTR(body), RSTRING_LEN(body)); + } + if (!ctx->req.has_transfer_encoding) { + char buf[64]; + size_t l = (size_t)sprintf(buf, "content-length: %zu\r\n", ctx->req.body.len); + h2o_buffer_reserve(&ctx->req.buf, l); + append_to_buffer(&ctx->req.buf, buf, l); + } + } + } + + h2o_buffer_reserve(&ctx->req.buf, 2); + append_to_buffer(&ctx->req.buf, H2O_STRLIT("\r\n")); + + /* build request and connect */ + ctx->refs.request = h2o_mruby_create_data_instance( + mrb, mrb_ary_entry(generator->ctx->shared->constants, H2O_MRUBY_HTTP_REQUEST_CLASS), ctx, &request_type); + h2o_http1client_connect(&ctx->client, ctx, &generator->req->conn->ctx->proxy.client_ctx, url.host, h2o_url_get_port(&url), + url.scheme == &H2O_URL_SCHEME_HTTPS, on_connect); + + return ctx->refs.request; +} + +mrb_value h2o_mruby_http_join_response_callback(h2o_mruby_generator_t *generator, mrb_value receiver, mrb_value args, + int *next_action) +{ + mrb_state *mrb = generator->ctx->shared->mrb; + struct st_h2o_mruby_http_request_context_t *ctx; + + if (generator->req == NULL) + return create_downstream_closed_exception(mrb); + + if ((ctx = mrb_data_check_get_ptr(mrb, mrb_ary_entry(args, 0), &request_type)) == NULL) + return mrb_exc_new_str_lit(mrb, E_ARGUMENT_ERROR, "HttpRequest#join wrong self"); + + ctx->receiver = receiver; + *next_action = H2O_MRUBY_CALLBACK_NEXT_ACTION_ASYNC; + return mrb_nil_value(); +} + +mrb_value h2o_mruby_http_fetch_chunk_callback(h2o_mruby_generator_t *generator, mrb_value receiver, mrb_value args, + int *next_action) +{ + mrb_state *mrb = generator->ctx->shared->mrb; + struct st_h2o_mruby_http_request_context_t *ctx; + mrb_value ret; + + if (generator->req == NULL) + return create_downstream_closed_exception(mrb); + + if ((ctx = mrb_data_check_get_ptr(mrb, mrb_ary_entry(args, 0), &input_stream_type)) == NULL) + return mrb_exc_new_str_lit(mrb, E_ARGUMENT_ERROR, "_HttpInputStream#each wrong self"); + + if (ctx->resp.has_content) { + ret = build_chunk(ctx); + } else { + ctx->receiver = receiver; + *next_action = H2O_MRUBY_CALLBACK_NEXT_ACTION_ASYNC; + ret = mrb_nil_value(); + } + + return ret; +} + +h2o_mruby_http_request_context_t *h2o_mruby_http_set_shortcut(mrb_state *mrb, mrb_value obj, void (*cb)(h2o_mruby_generator_t *)) +{ + struct st_h2o_mruby_http_request_context_t *ctx; + + if ((ctx = mrb_data_check_get_ptr(mrb, obj, &input_stream_type)) == NULL) + return NULL; + ctx->shortcut_notify_cb = cb; + return ctx; +} + +h2o_buffer_t **h2o_mruby_http_peek_content(h2o_mruby_http_request_context_t *ctx, int *is_final) +{ + *is_final = ctx->client == NULL; + return ctx->client != NULL && ctx->resp.has_content ? &ctx->client->sock->input : &ctx->resp.after_closed; +} + +void h2o_mruby_http_request_init_context(h2o_mruby_shared_context_t *ctx) +{ + mrb_state *mrb = ctx->mrb; + + h2o_mruby_eval_expr(mrb, H2O_MRUBY_CODE_HTTP_REQUEST); + h2o_mruby_assert(mrb); + + struct RClass *module, *klass; + module = mrb_define_module(mrb, "H2O"); + + mrb_define_method(mrb, mrb->kernel_module, "http_request", http_request_method, MRB_ARGS_ARG(1, 2)); + + klass = mrb_class_get_under(mrb, module, "HttpRequest"); + mrb_ary_set(mrb, ctx->constants, H2O_MRUBY_HTTP_REQUEST_CLASS, mrb_obj_value(klass)); + + klass = mrb_class_get_under(mrb, module, "HttpInputStream"); + mrb_ary_set(mrb, ctx->constants, H2O_MRUBY_HTTP_INPUT_STREAM_CLASS, mrb_obj_value(klass)); + + klass = mrb_class_get_under(mrb, klass, "Empty"); + mrb_ary_set(mrb, ctx->constants, H2O_MRUBY_HTTP_EMPTY_INPUT_STREAM_CLASS, mrb_obj_value(klass)); + + h2o_mruby_define_callback(mrb, "_h2o__http_join_response", H2O_MRUBY_CALLBACK_ID_HTTP_JOIN_RESPONSE); + h2o_mruby_define_callback(mrb, "_h2o__http_fetch_chunk", H2O_MRUBY_CALLBACK_ID_HTTP_FETCH_CHUNK); +} |