diff --git a/.gitignore b/.gitignore index 7adeac3d..7ed31ec3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.lo *.m4 *.o +*.pyc .deps/ .libs/ INSTALL @@ -31,10 +32,10 @@ test-driver # test logs generated by `make check` *.log *.trs - -lib/MSVC_obj/ -_VC_ROOT/ -.depend.MSVC -*.pyd -*.egg-info/ -python/nghttp2.c + +lib/MSVC_obj/ +_VC_ROOT/ +.depend.MSVC +*.pyd +*.egg-info/ +python/nghttp2.c diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..301b5b3c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third-party/mruby"] + path = third-party/mruby + url = https://github.com/mruby/mruby diff --git a/.travis.yml b/.travis.yml index 78e69d34..1dee67b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,8 @@ before_script: - autoreconf -i - automake - autoconf - - ./configure --enable-werror + - git submodule update --init + - ./configure --enable-werror --with-mruby script: - make - make check diff --git a/README.rst b/README.rst index bffaed04..d69918ff 100644 --- a/README.rst +++ b/README.rst @@ -113,6 +113,16 @@ If you are using Ubuntu 14.04 LTS (trusty), run the following to install the nee spdylay is not packaged in Ubuntu, so you need to build it yourself: http://tatsuhiro-t.github.io/spdylay/ +To enable mruby support for nghttpx, `mruby +`_ is required. We need to build +mruby with C++ ABI explicitly turned on, and probably need other +mrgems, mruby is manged by git submodule under third-party/mruby +directory. Currently, mruby support for nghttpx is disabled by +default. To enable mruby support, use ``--with-mruby`` configure +option. Note that at the time of this writing, libmruby-dev and mruby +packages in Debian/Ubuntu are not usable for nghttp2, since they do +not enable C++ ABI. + Building from git ----------------- @@ -127,6 +137,12 @@ used:: To compile the source code, gcc >= 4.8.3 or clang >= 3.4 is required. +.. note:: + + To enable mruby support in nghttpx, run ``git submodule update + --init`` before running configure script, and use ``--with-mruby`` + configure option. + .. note:: Mac OS X users may need the ``--disable-threads`` configure option to diff --git a/configure.ac b/configure.ac index 31a38818..ce4a575f 100644 --- a/configure.ac +++ b/configure.ac @@ -119,6 +119,11 @@ AC_ARG_WITH([spdylay], [Use spdylay [default=check]])], [request_spdylay=$withval], [request_spdylay=check]) +AC_ARG_WITH([mruby], + [AS_HELP_STRING([--with-mruby], + [Use mruby [default=no]])], + [request_mruby=$withval], [request_mruby=no]) + AC_ARG_WITH([cython], [AS_HELP_STRING([--with-cython=PATH], [Use cython in given PATH])], @@ -370,6 +375,19 @@ fi AM_CONDITIONAL([HAVE_SPDYLAY], [ test "x${have_spdylay}" = "xyes" ]) +# mruby (for src/nghttpx) +if test "x${request_mruby}" = "xyes"; then + # We are going to build mruby + have_mruby=yes + AC_DEFINE([HAVE_MRUBY], [1], [Define to 1 if you have `mruby` library.]) + LIBMRUBY_LIBS="-lmruby -lm" + LIBMRUBY_CFLAGS= + AC_SUBST([LIBMRUBY_LIBS]) + AC_SUBST([LIBMRUBY_CFLAGS]) +fi + +AM_CONDITIONAL([HAVE_MRUBY], [test "x${have_mruby}" = "xyes"]) + # Check Boost Asio library have_asio_lib=no @@ -717,6 +735,7 @@ AC_MSG_NOTICE([summary of build options: Libev: ${have_libev} Libevent(SSL): ${have_libevent_openssl} Spdylay: ${have_spdylay} + MRuby: ${have_mruby} Jansson: ${have_jansson} Jemalloc: ${have_jemalloc} Zlib: ${have_zlib} diff --git a/doc/_exts/sphinxcontrib/LICENSE.rubydomain b/doc/_exts/sphinxcontrib/LICENSE.rubydomain new file mode 100644 index 00000000..a560d759 --- /dev/null +++ b/doc/_exts/sphinxcontrib/LICENSE.rubydomain @@ -0,0 +1,28 @@ +If not otherwise noted, the extensions in this package are licensed +under the following license. + +Copyright (c) 2010 by the contributors (see AUTHORS file). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/doc/_exts/sphinxcontrib/__init__.py b/doc/_exts/sphinxcontrib/__init__.py new file mode 100644 index 00000000..b5a7dc29 --- /dev/null +++ b/doc/_exts/sphinxcontrib/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" + sphinxcontrib + ~~~~~~~~~~~~~ + + This package is a namespace package that contains all extensions + distributed in the ``sphinx-contrib`` distribution. + + :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +__import__('pkg_resources').declare_namespace(__name__) + diff --git a/doc/_exts/sphinxcontrib/rubydomain.py b/doc/_exts/sphinxcontrib/rubydomain.py new file mode 100644 index 00000000..bba02f5d --- /dev/null +++ b/doc/_exts/sphinxcontrib/rubydomain.py @@ -0,0 +1,695 @@ +# -*- coding: utf-8 -*- +""" + sphinx.domains.ruby + ~~~~~~~~~~~~~~~~~~~ + + The Ruby domain. + + :copyright: Copyright 2010 by SHIBUKAWA Yoshiki + :license: BSD, see LICENSE for details. +""" + +import re + +from docutils import nodes +from docutils.parsers.rst import directives + +from sphinx import addnodes +from sphinx.roles import XRefRole +from sphinx.locale import l_, _ +from sphinx.domains import Domain, ObjType, Index +from sphinx.directives import ObjectDescription +from sphinx.util.nodes import make_refnode +from sphinx.util.compat import Directive +from sphinx.util.docfields import Field, GroupedField, TypedField + + +# REs for Ruby signatures +rb_sig_re = re.compile( + r'''^ ([\w.]*\.)? # class name(s) + (\$?\w+\??!?) \s* # thing name + (?: \((.*)\) # optional: arguments + (?:\s* -> \s* (.*))? # return annotation + )? $ # and nothing more + ''', re.VERBOSE) + +rb_paramlist_re = re.compile(r'([\[\],])') # split at '[', ']' and ',' + +separators = { + 'method':'#', 'attr_reader':'#', 'attr_writer':'#', 'attr_accessor':'#', + 'function':'.', 'classmethod':'.', 'class':'::', 'module':'::', + 'global':'', 'const':'::'} + +rb_separator = re.compile(r"(?:\w+)?(?:::)?(?:\.)?(?:#)?") + + +def _iteritems(d): + + for k in d: + yield k, d[k] + + +def ruby_rsplit(fullname): + items = [item for item in rb_separator.findall(fullname)] + return ''.join(items[:-2]), items[-1] + + +class RubyObject(ObjectDescription): + """ + Description of a general Ruby object. + """ + option_spec = { + 'noindex': directives.flag, + 'module': directives.unchanged, + } + + doc_field_types = [ + TypedField('parameter', label=l_('Parameters'), + names=('param', 'parameter', 'arg', 'argument'), + typerolename='obj', typenames=('paramtype', 'type')), + TypedField('variable', label=l_('Variables'), rolename='obj', + names=('var', 'ivar', 'cvar'), + typerolename='obj', typenames=('vartype',)), + GroupedField('exceptions', label=l_('Raises'), rolename='exc', + names=('raises', 'raise', 'exception', 'except'), + can_collapse=True), + Field('returnvalue', label=l_('Returns'), has_arg=False, + names=('returns', 'return')), + Field('returntype', label=l_('Return type'), has_arg=False, + names=('rtype',)), + ] + + def get_signature_prefix(self, sig): + """ + May return a prefix to put before the object name in the signature. + """ + return '' + + def needs_arglist(self): + """ + May return true if an empty argument list is to be generated even if + the document contains none. + """ + return False + + def handle_signature(self, sig, signode): + """ + Transform a Ruby signature into RST nodes. + Returns (fully qualified name of the thing, classname if any). + + If inside a class, the current class name is handled intelligently: + * it is stripped from the displayed name if present + * it is added to the full name (return value) if not present + """ + m = rb_sig_re.match(sig) + if m is None: + raise ValueError + name_prefix, name, arglist, retann = m.groups() + if not name_prefix: + name_prefix = "" + # determine module and class name (if applicable), as well as full name + modname = self.options.get( + 'module', self.env.temp_data.get('rb:module')) + classname = self.env.temp_data.get('rb:class') + if self.objtype == 'global': + add_module = False + modname = None + classname = None + fullname = name + elif classname: + add_module = False + if name_prefix and name_prefix.startswith(classname): + fullname = name_prefix + name + # class name is given again in the signature + name_prefix = name_prefix[len(classname):].lstrip('.') + else: + separator = separators[self.objtype] + fullname = classname + separator + name_prefix + name + else: + add_module = True + if name_prefix: + classname = name_prefix.rstrip('.') + fullname = name_prefix + name + else: + classname = '' + fullname = name + + signode['module'] = modname + signode['class'] = self.class_name = classname + signode['fullname'] = fullname + + sig_prefix = self.get_signature_prefix(sig) + if sig_prefix: + signode += addnodes.desc_annotation(sig_prefix, sig_prefix) + + if name_prefix: + signode += addnodes.desc_addname(name_prefix, name_prefix) + # exceptions are a special case, since they are documented in the + # 'exceptions' module. + elif add_module and self.env.config.add_module_names: + if self.objtype == 'global': + nodetext = '' + signode += addnodes.desc_addname(nodetext, nodetext) + else: + modname = self.options.get( + 'module', self.env.temp_data.get('rb:module')) + if modname and modname != 'exceptions': + nodetext = modname + separators[self.objtype] + signode += addnodes.desc_addname(nodetext, nodetext) + + signode += addnodes.desc_name(name, name) + if not arglist: + if self.needs_arglist(): + # for callables, add an empty parameter list + signode += addnodes.desc_parameterlist() + if retann: + signode += addnodes.desc_returns(retann, retann) + return fullname, name_prefix + signode += addnodes.desc_parameterlist() + + stack = [signode[-1]] + for token in rb_paramlist_re.split(arglist): + if token == '[': + opt = addnodes.desc_optional() + stack[-1] += opt + stack.append(opt) + elif token == ']': + try: + stack.pop() + except IndexError: + raise ValueError + elif not token or token == ',' or token.isspace(): + pass + else: + token = token.strip() + stack[-1] += addnodes.desc_parameter(token, token) + if len(stack) != 1: + raise ValueError + if retann: + signode += addnodes.desc_returns(retann, retann) + return fullname, name_prefix + + def get_index_text(self, modname, name): + """ + Return the text for the index entry of the object. + """ + raise NotImplementedError('must be implemented in subclasses') + + def _is_class_member(self): + return self.objtype.endswith('method') or self.objtype.startswith('attr') + + def add_target_and_index(self, name_cls, sig, signode): + if self.objtype == 'global': + modname = '' + else: + modname = self.options.get( + 'module', self.env.temp_data.get('rb:module')) + separator = separators[self.objtype] + if self._is_class_member(): + if signode['class']: + prefix = modname and modname + '::' or '' + else: + prefix = modname and modname + separator or '' + else: + prefix = modname and modname + separator or '' + fullname = prefix + name_cls[0] + # note target + if fullname not in self.state.document.ids: + signode['names'].append(fullname) + signode['ids'].append(fullname) + signode['first'] = (not self.names) + self.state.document.note_explicit_target(signode) + objects = self.env.domaindata['rb']['objects'] + if fullname in objects: + self.env.warn( + self.env.docname, + 'duplicate object description of %s, ' % fullname + + 'other instance in ' + + self.env.doc2path(objects[fullname][0]), + self.lineno) + objects[fullname] = (self.env.docname, self.objtype) + + indextext = self.get_index_text(modname, name_cls) + if indextext: + self.indexnode['entries'].append(('single', indextext, + fullname, fullname)) + + def before_content(self): + # needed for automatic qualification of members (reset in subclasses) + self.clsname_set = False + + def after_content(self): + if self.clsname_set: + self.env.temp_data['rb:class'] = None + + +class RubyModulelevel(RubyObject): + """ + Description of an object on module level (functions, data). + """ + + def needs_arglist(self): + return self.objtype == 'function' + + def get_index_text(self, modname, name_cls): + if self.objtype == 'function': + if not modname: + return _('%s() (global function)') % name_cls[0] + return _('%s() (module function in %s)') % (name_cls[0], modname) + else: + return '' + + +class RubyGloballevel(RubyObject): + """ + Description of an object on module level (functions, data). + """ + + def get_index_text(self, modname, name_cls): + if self.objtype == 'global': + return _('%s (global variable)') % name_cls[0] + else: + return '' + + +class RubyEverywhere(RubyObject): + """ + Description of a class member (methods, attributes). + """ + + def needs_arglist(self): + return self.objtype == 'method' + + def get_index_text(self, modname, name_cls): + name, cls = name_cls + add_modules = self.env.config.add_module_names + if self.objtype == 'method': + try: + clsname, methname = ruby_rsplit(name) + except ValueError: + if modname: + return _('%s() (in module %s)') % (name, modname) + else: + return '%s()' % name + if modname and add_modules: + return _('%s() (%s::%s method)') % (methname, modname, + clsname) + else: + return _('%s() (%s method)') % (methname, clsname) + else: + return '' + + +class RubyClasslike(RubyObject): + """ + Description of a class-like object (classes, exceptions). + """ + + def get_signature_prefix(self, sig): + return self.objtype + ' ' + + def get_index_text(self, modname, name_cls): + if self.objtype == 'class': + if not modname: + return _('%s (class)') % name_cls[0] + return _('%s (class in %s)') % (name_cls[0], modname) + elif self.objtype == 'exception': + return name_cls[0] + else: + return '' + + def before_content(self): + RubyObject.before_content(self) + if self.names: + self.env.temp_data['rb:class'] = self.names[0][0] + self.clsname_set = True + + +class RubyClassmember(RubyObject): + """ + Description of a class member (methods, attributes). + """ + + def needs_arglist(self): + return self.objtype.endswith('method') + + def get_signature_prefix(self, sig): + if self.objtype == 'classmethod': + return "classmethod %s." % self.class_name + elif self.objtype == 'attr_reader': + return "attribute [R] " + elif self.objtype == 'attr_writer': + return "attribute [W] " + elif self.objtype == 'attr_accessor': + return "attribute [R/W] " + return '' + + def get_index_text(self, modname, name_cls): + name, cls = name_cls + add_modules = self.env.config.add_module_names + if self.objtype == 'classmethod': + try: + clsname, methname = ruby_rsplit(name) + except ValueError: + return '%s()' % name + if modname: + return _('%s() (%s.%s class method)') % (methname, modname, + clsname) + else: + return _('%s() (%s class method)') % (methname, clsname) + elif self.objtype.startswith('attr'): + try: + clsname, attrname = ruby_rsplit(name) + except ValueError: + return name + if modname and add_modules: + return _('%s (%s.%s attribute)') % (attrname, modname, clsname) + else: + return _('%s (%s attribute)') % (attrname, clsname) + else: + return '' + + def before_content(self): + RubyObject.before_content(self) + lastname = self.names and self.names[-1][1] + if lastname and not self.env.temp_data.get('rb:class'): + self.env.temp_data['rb:class'] = lastname.strip('.') + self.clsname_set = True + + +class RubyModule(Directive): + """ + Directive to mark description of a new module. + """ + + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = False + option_spec = { + 'platform': lambda x: x, + 'synopsis': lambda x: x, + 'noindex': directives.flag, + 'deprecated': directives.flag, + } + + def run(self): + env = self.state.document.settings.env + modname = self.arguments[0].strip() + noindex = 'noindex' in self.options + env.temp_data['rb:module'] = modname + env.domaindata['rb']['modules'][modname] = \ + (env.docname, self.options.get('synopsis', ''), + self.options.get('platform', ''), 'deprecated' in self.options) + targetnode = nodes.target('', '', ids=['module-' + modname], ismod=True) + self.state.document.note_explicit_target(targetnode) + ret = [targetnode] + # XXX this behavior of the module directive is a mess... + if 'platform' in self.options: + platform = self.options['platform'] + node = nodes.paragraph() + node += nodes.emphasis('', _('Platforms: ')) + node += nodes.Text(platform, platform) + ret.append(node) + # the synopsis isn't printed; in fact, it is only used in the + # modindex currently + if not noindex: + indextext = _('%s (module)') % modname + inode = addnodes.index(entries=[('single', indextext, + 'module-' + modname, modname)]) + ret.append(inode) + return ret + + +class RubyCurrentModule(Directive): + """ + This directive is just to tell Sphinx that we're documenting + stuff in module foo, but links to module foo won't lead here. + """ + + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = False + option_spec = {} + + def run(self): + env = self.state.document.settings.env + modname = self.arguments[0].strip() + if modname == 'None': + env.temp_data['rb:module'] = None + else: + env.temp_data['rb:module'] = modname + return [] + + +class RubyXRefRole(XRefRole): + def process_link(self, env, refnode, has_explicit_title, title, target): + if not has_explicit_title: + title = title.lstrip('.') # only has a meaning for the target + title = title.lstrip('#') + if title.startswith("::"): + title = title[2:] + target = target.lstrip('~') # only has a meaning for the title + # if the first character is a tilde, don't display the module/class + # parts of the contents + if title[0:1] == '~': + m = re.search(r"(?:\.)?(?:#)?(?:::)?(.*)\Z", title) + if m: + title = m.group(1) + if not title.startswith("$"): + refnode['rb:module'] = env.temp_data.get('rb:module') + refnode['rb:class'] = env.temp_data.get('rb:class') + # if the first character is a dot, search more specific namespaces first + # else search builtins first + if target[0:1] == '.': + target = target[1:] + refnode['refspecific'] = True + return title, target + + +class RubyModuleIndex(Index): + """ + Index subclass to provide the Ruby module index. + """ + + name = 'modindex' + localname = l_('Ruby Module Index') + shortname = l_('modules') + + def generate(self, docnames=None): + content = {} + # list of prefixes to ignore + ignores = self.domain.env.config['modindex_common_prefix'] + ignores = sorted(ignores, key=len, reverse=True) + # list of all modules, sorted by module name + modules = sorted(_iteritems(self.domain.data['modules']), + key=lambda x: x[0].lower()) + # sort out collapsable modules + prev_modname = '' + num_toplevels = 0 + for modname, (docname, synopsis, platforms, deprecated) in modules: + if docnames and docname not in docnames: + continue + + for ignore in ignores: + if modname.startswith(ignore): + modname = modname[len(ignore):] + stripped = ignore + break + else: + stripped = '' + + # we stripped the whole module name? + if not modname: + modname, stripped = stripped, '' + + entries = content.setdefault(modname[0].lower(), []) + + package = modname.split('::')[0] + if package != modname: + # it's a submodule + if prev_modname == package: + # first submodule - make parent a group head + entries[-1][1] = 1 + elif not prev_modname.startswith(package): + # submodule without parent in list, add dummy entry + entries.append([stripped + package, 1, '', '', '', '', '']) + subtype = 2 + else: + num_toplevels += 1 + subtype = 0 + + qualifier = deprecated and _('Deprecated') or '' + entries.append([stripped + modname, subtype, docname, + 'module-' + stripped + modname, platforms, + qualifier, synopsis]) + prev_modname = modname + + # apply heuristics when to collapse modindex at page load: + # only collapse if number of toplevel modules is larger than + # number of submodules + collapse = len(modules) - num_toplevels < num_toplevels + + # sort by first letter + content = sorted(_iteritems(content)) + + return content, collapse + + +class RubyDomain(Domain): + """Ruby language domain.""" + name = 'rb' + label = 'Ruby' + object_types = { + 'function': ObjType(l_('function'), 'func', 'obj'), + 'global': ObjType(l_('global variable'), 'global', 'obj'), + 'method': ObjType(l_('method'), 'meth', 'obj'), + 'class': ObjType(l_('class'), 'class', 'obj'), + 'exception': ObjType(l_('exception'), 'exc', 'obj'), + 'classmethod': ObjType(l_('class method'), 'meth', 'obj'), + 'attr_reader': ObjType(l_('attribute'), 'attr', 'obj'), + 'attr_writer': ObjType(l_('attribute'), 'attr', 'obj'), + 'attr_accessor': ObjType(l_('attribute'), 'attr', 'obj'), + 'const': ObjType(l_('const'), 'const', 'obj'), + 'module': ObjType(l_('module'), 'mod', 'obj'), + } + + directives = { + 'function': RubyModulelevel, + 'global': RubyGloballevel, + 'method': RubyEverywhere, + 'const': RubyEverywhere, + 'class': RubyClasslike, + 'exception': RubyClasslike, + 'classmethod': RubyClassmember, + 'attr_reader': RubyClassmember, + 'attr_writer': RubyClassmember, + 'attr_accessor': RubyClassmember, + 'module': RubyModule, + 'currentmodule': RubyCurrentModule, + } + + roles = { + 'func': RubyXRefRole(fix_parens=False), + 'global':RubyXRefRole(), + 'class': RubyXRefRole(), + 'exc': RubyXRefRole(), + 'meth': RubyXRefRole(fix_parens=False), + 'attr': RubyXRefRole(), + 'const': RubyXRefRole(), + 'mod': RubyXRefRole(), + 'obj': RubyXRefRole(), + } + initial_data = { + 'objects': {}, # fullname -> docname, objtype + 'modules': {}, # modname -> docname, synopsis, platform, deprecated + } + indices = [ + RubyModuleIndex, + ] + + def clear_doc(self, docname): + for fullname, (fn, _) in list(self.data['objects'].items()): + if fn == docname: + del self.data['objects'][fullname] + for modname, (fn, _, _, _) in list(self.data['modules'].items()): + if fn == docname: + del self.data['modules'][modname] + + def find_obj(self, env, modname, classname, name, type, searchorder=0): + """ + Find a Ruby object for "name", perhaps using the given module and/or + classname. + """ + # skip parens + if name[-2:] == '()': + name = name[:-2] + + if not name: + return None, None + + objects = self.data['objects'] + + newname = None + if searchorder == 1: + if modname and classname and \ + modname + '::' + classname + '#' + name in objects: + newname = modname + '::' + classname + '#' + name + elif modname and classname and \ + modname + '::' + classname + '.' + name in objects: + newname = modname + '::' + classname + '.' + name + elif modname and modname + '::' + name in objects: + newname = modname + '::' + name + elif modname and modname + '#' + name in objects: + newname = modname + '#' + name + elif modname and modname + '.' + name in objects: + newname = modname + '.' + name + elif classname and classname + '.' + name in objects: + newname = classname + '.' + name + elif classname and classname + '#' + name in objects: + newname = classname + '#' + name + elif name in objects: + newname = name + else: + if name in objects: + newname = name + elif classname and classname + '.' + name in objects: + newname = classname + '.' + name + elif classname and classname + '#' + name in objects: + newname = classname + '#' + name + elif modname and modname + '::' + name in objects: + newname = modname + '::' + name + elif modname and modname + '#' + name in objects: + newname = modname + '#' + name + elif modname and modname + '.' + name in objects: + newname = modname + '.' + name + elif modname and classname and \ + modname + '::' + classname + '#' + name in objects: + newname = modname + '::' + classname + '#' + name + elif modname and classname and \ + modname + '::' + classname + '.' + name in objects: + newname = modname + '::' + classname + '.' + name + # special case: object methods + elif type in ('func', 'meth') and '.' not in name and \ + 'object.' + name in objects: + newname = 'object.' + name + if newname is None: + return None, None + return newname, objects[newname] + + def resolve_xref(self, env, fromdocname, builder, + typ, target, node, contnode): + if (typ == 'mod' or + typ == 'obj' and target in self.data['modules']): + docname, synopsis, platform, deprecated = \ + self.data['modules'].get(target, ('','','', '')) + if not docname: + return None + else: + title = '%s%s%s' % ((platform and '(%s) ' % platform), + synopsis, + (deprecated and ' (deprecated)' or '')) + return make_refnode(builder, fromdocname, docname, + 'module-' + target, contnode, title) + else: + modname = node.get('rb:module') + clsname = node.get('rb:class') + searchorder = node.hasattr('refspecific') and 1 or 0 + name, obj = self.find_obj(env, modname, clsname, + target, typ, searchorder) + if not obj: + return None + else: + return make_refnode(builder, fromdocname, obj[0], name, + contnode, name) + + def get_objects(self): + for modname, info in _iteritems(self.data['modules']): + yield (modname, modname, 'module', info[0], 'module-' + modname, 0) + for refname, (docname, type) in _iteritems(self.data['objects']): + yield (refname, refname, type, docname, refname, 1) + + +def setup(app): + app.add_domain(RubyDomain) diff --git a/doc/conf.py.in b/doc/conf.py.in index 0e572cf3..e6bd3203 100644 --- a/doc/conf.py.in +++ b/doc/conf.py.in @@ -41,6 +41,8 @@ import sys, os # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) +sys.path.append(os.path.abspath('_exts')) + # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. @@ -48,7 +50,7 @@ import sys, os # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [] +extensions = ['sphinxcontrib.rubydomain'] # Add any paths that contain templates here, relative to this directory. templates_path = ['@top_srcdir@/_templates'] diff --git a/doc/nghttpx.h2r b/doc/nghttpx.h2r index ac089c37..c3e6dec5 100644 --- a/doc/nghttpx.h2r +++ b/doc/nghttpx.h2r @@ -153,6 +153,239 @@ from the given file. In this case, nghttpx does not rotate key automatically. To rotate key, one has to restart nghttpx (see SIGNALS). +MRUBY SCRIPTING +--------------- + +.. warning:: + + The current mruby extension API is experimental and not frozen. The + API is subject to change in the future release. + +nghttpx allows users to extend its capability using mruby scripts. +nghttpx has 2 hook points to execute mruby script: request phase and +response phase. The request phase hook is invoked after all request +header fields are received from client. The response phase hook is +invoked after all response header fields are received from backend +server. These hooks allows users to modify header fields, or common +HTTP variables, like authority or request path, and even return custom +response without forwarding request to backend servers. + +To set request phase hook, use :option:`--request-phase-file` option. +To set response phase hook, use :option:`--response-phase-file` +option. + +For request and response phase hook, user calls :rb:meth:`Nghttpx.run` +with block. The :rb:class:`Nghttpx::Env` is passed to the block. +User can can access :rb:class:`Nghttpx::Request` and +:rb:class:`Nghttpx::Response` objects via :rb:attr:`Nghttpx::Env#req` +and :rb:attr:`Nghttpx::Env#resp` respectively. + +.. rb:module:: Nghttpx + +.. rb:classmethod:: run(&block) + + Run request or response phase hook with given *block*. + :rb:class:`Nghttpx::Env` object is passed to the given block. + +.. rb:const:: REQUEST_PHASE + + Constant to represent request phase. + +.. rb:const:: RESPONSE_PHASE + + Constant to represent response phase. + +.. rb:class:: Env + + Object to represent current request specific context. + + .. rb:attr_reader:: req + + Return :rb:class:`Request` object. + + .. rb:attr_reader:: resp + + Return :rb:class:`Response` object. + + .. rb:attr_reader:: ctx + + Return Ruby hash object. It persists until request finishes. + So values set in request phase hoo can be retrieved in + response phase hook. + + .. rb:attr_reader:: phase + + Return the current phase. + + .. rb:attr_reader:: remote_addr + + Return IP address of a remote client. + +.. rb:class:: Request + + Object to represent request from client. The modification to + Request object is allowed only in request phase hook. + + .. rb:attr_reader:: http_version_major + + Return HTTP major version. + + .. rb:attr_reader:: http_version_minor + + Return HTTP minor version. + + .. rb:attr_accessor:: method + + HTTP method. On assignment, copy of given value is assigned. + We don't accept arbitrary method name. We will document them + later, but well known methods, like GET, PUT and POST, are all + supported. + + .. rb:attr_accessor:: authority + + Authority (i.e., example.org), including optional port + component . On assignment, copy of given value is assigned. + + .. rb:attr_accessor:: scheme + + Scheme (i.e., http, https). On assignment, copy of given + value is assigned. + + .. rb:attr_accessor:: path + + Request path, including query component (i.e., /index.html). + On assignment, copy of given value is assigned. The path does + not include authority component of URI. + + .. rb:attr_reader:: headers + + Return Ruby hash containing copy of request header fields. + Changing values in returned hash does not change request + header fields actually used in request processing. Use + :rb:meth:`Nghttpx::Request#add_header` or + :rb:meth:`Nghttpx::Request#set_header` to change request + header fields. + + .. rb:method:: add_header(key, value) + + Add header entry associated with key. The value can be single + string or array of string. It does not replace any existing + values associated with key. + + .. rb:method:: set_header(key, value) + + Set header entry associated with key. The value can be single + string or array of string. It replaces any existing values + associated with key. + + .. rb:method:: clear_headers + + Clear all existing request header fields. + + .. rb:method:: push uri + + Initiate to push resource identified by *uri*. Only HTTP/2 + protocol supports this feature. For the other protocols, this + method is noop. *uri* can be absolute URI, absolute path or + relative path to the current request. For absolute or + relative path, scheme and authority are inherited from the + current request. Currently, method is always GET. nghttpx + will issue request to backend servers to fulfill this request. + The request and response phase hooks will be called for pushed + resource as well. + +.. rb:class:: Response + + Object to represent response from backend server. + + .. rb:attr_reader:: http_version_major + + Return HTTP major version. + + .. rb:attr_reader:: http_version_minor + + Return HTTP minor version. + + .. rb:attr_accessor:: status + + HTTP status code. It must be in the range [200, 999], + inclusive. The non-final status code is not supported in + mruby scripting at the moment. + + .. rb:attr_reader:: headers + + Return Ruby hash containing copy of response header fields. + Changing values in returned hash does not change response + header fields actually used in response processing. Use + :rb:meth:`Nghttpx::Response#add_header` or + :rb:meth:`Nghttpx::Response#set_header` to change response + header fields. + + .. rb:method:: add_header(key, value) + + Add header entry associated with key. The value can be single + string or array of string. It does not replace any existing + values associated with key. + + .. rb:method:: set_header(key, value) + + Set header entry associated with key. The value can be single + string or array of string. It replaces any existing values + associated with key. + + .. rb:method:: clear_headers + + Clear all existing response header fields. + + .. rb:method:: return(body) + + Return custom response *body* to a client. When this method + is called in request phase hook, the request is not forwarded + to the backend, and response phase hook for this request will + not be invoked. When this method is called in resonse phase + hook, response from backend server is canceled and discarded. + The status code and response header fields should be set + before using this method. To set status code, use :rb:meth To + set response header fields, use + :rb:attr:`Nghttpx::Response#status`. If status code is not + set, 200 is used. :rb:meth:`Nghttpx::Response#add_header` and + :rb:meth:`Nghttpx::Response#set_header`. When this method is + invoked in response phase hook, the response headers are + filled with the ones received from backend server. To send + completely custom header fields, first call + :rb:meth:`Nghttpx::Response#clear_headers` to erase all + existing header fields, and then add required header fields. + It is an error to call this method twice for a given request. + +MRUBY EXAMPLES +~~~~~~~~~~~~~~ + +Modify requet path: + +.. code-block:: ruby + + Nghttpx.run do |env| + env.req.path = "/apps#{env.req.path}" + end + +Note that the file containing the above script must be set with +:option:`--request-phase-file` option since we modify request path. + +Restrict permission of viewing a content to a specific client +addresses: + +.. code-block:: ruby + + Nghttpx.run do |env| + allowed_clients = ["127.0.0.1", "::1"] + + if env.req.path.start_with?("/log/") && + !allowed_clients.include?(env.remote_addr) then + env.resp.status = 404 + env.resp.return "permission denied" + end + end + SEE ALSO -------- diff --git a/gennghttpxfun.py b/gennghttpxfun.py index 4e99155c..3114137d 100755 --- a/gennghttpxfun.py +++ b/gennghttpxfun.py @@ -98,6 +98,8 @@ OPTIONS = [ "tls-ticket-key-memcached-interval", "tls-ticket-key-memcached-max-retry", "tls-ticket-key-memcached-max-fail", + "request-phase-file", + "response-phase-file", "conf", ] diff --git a/integration-tests/Makefile.am b/integration-tests/Makefile.am index 3ad95ece..5159e038 100644 --- a/integration-tests/Makefile.am +++ b/integration-tests/Makefile.am @@ -30,7 +30,10 @@ EXTRA_DIST = \ server.crt \ alt-server.key \ alt-server.crt \ - setenv + setenv \ + req-set-header.rb \ + resp-set-header.rb \ + return.rb itprep-local: go get -d -v github.com/bradfitz/http2 diff --git a/integration-tests/nghttpx_http1_test.go b/integration-tests/nghttpx_http1_test.go index 71aeeb85..c7658eb5 100644 --- a/integration-tests/nghttpx_http1_test.go +++ b/integration-tests/nghttpx_http1_test.go @@ -355,6 +355,120 @@ func TestH1H1Websocket(t *testing.T) { } } +// TestH1H1ReqPhaseSetHeader tests mruby request phase hook +// modifies request header fields. +func TestH1H1ReqPhaseSetHeader(t *testing.T) { + st := newServerTester([]string{"--request-phase-file=" + testDir + "/req-set-header.rb"}, t, func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("User-Agent"), "mruby"; got != want { + t.Errorf("User-Agent = %v; want %v", got, want) + } + }) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1ReqPhaseSetHeader", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, 200; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH1H1ReqPhaseReturn tests mruby request phase hook returns +// custom response. +func TestH1H1ReqPhaseReturn(t *testing.T) { + st := newServerTester([]string{"--request-phase-file=" + testDir + "/return.rb"}, t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1ReqPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, 404; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "11"}, + {"from", "mruby"}, + } + for _, tt := range hdtests { + if got, want := res.header.Get(tt.k), tt.v; got != want { + t.Errorf("%v = %v; want %v", tt.k, got, want) + } + } + + if got, want := string(res.body), "Hello World"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + +// TestH1H1RespPhaseSetHeader tests mruby response phase hook modifies +// response header fields. +func TestH1H1RespPhaseSetHeader(t *testing.T) { + st := newServerTester([]string{"--response-phase-file=" + testDir + "/resp-set-header.rb"}, t, noopHandler) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1RespPhaseSetHeader", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, 200; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + if got, want := res.header.Get("alpha"), "bravo"; got != want { + t.Errorf("alpha = %v; want %v", got, want) + } +} + +// TestH1H1RespPhaseReturn tests mruby response phase hook returns +// custom response. +func TestH1H1RespPhaseReturn(t *testing.T) { + st := newServerTester([]string{"--response-phase-file=" + testDir + "/return.rb"}, t, noopHandler) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H1RespPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, 404; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "11"}, + {"from", "mruby"}, + } + for _, tt := range hdtests { + if got, want := res.header.Get(tt.k), tt.v; got != want { + t.Errorf("%v = %v; want %v", tt.k, got, want) + } + } + + if got, want := string(res.body), "Hello World"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + // TestH1H2ConnectFailure tests that server handles the situation that // connection attempt to HTTP/2 backend failed. func TestH1H2ConnectFailure(t *testing.T) { @@ -547,3 +661,73 @@ func TestH1H2NoVia(t *testing.T) { t.Errorf("Via: %v; want %v", got, want) } } + +// TestH1H2ReqPhaseReturn tests mruby request phase hook returns +// custom response. +func TestH1H2ReqPhaseReturn(t *testing.T) { + st := newServerTester([]string{"--http2-bridge", "--request-phase-file=" + testDir + "/return.rb"}, t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H2ReqPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, 404; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "11"}, + {"from", "mruby"}, + } + for _, tt := range hdtests { + if got, want := res.header.Get(tt.k), tt.v; got != want { + t.Errorf("%v = %v; want %v", tt.k, got, want) + } + } + + if got, want := string(res.body), "Hello World"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + +// TestH1H2RespPhaseReturn tests mruby response phase hook returns +// custom response. +func TestH1H2RespPhaseReturn(t *testing.T) { + st := newServerTester([]string{"--http2-bridge", "--response-phase-file=" + testDir + "/return.rb"}, t, noopHandler) + defer st.Close() + + res, err := st.http1(requestParam{ + name: "TestH1H2RespPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.http1() = %v", err) + } + + if got, want := res.status, 404; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "11"}, + {"from", "mruby"}, + } + for _, tt := range hdtests { + if got, want := res.header.Get(tt.k), tt.v; got != want { + t.Errorf("%v = %v; want %v", tt.k, got, want) + } + } + + if got, want := string(res.body), "Hello World"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} diff --git a/integration-tests/nghttpx_http2_test.go b/integration-tests/nghttpx_http2_test.go index 5fe4c3db..41c527f7 100644 --- a/integration-tests/nghttpx_http2_test.go +++ b/integration-tests/nghttpx_http2_test.go @@ -640,6 +640,120 @@ func TestH2H1HeaderFields(t *testing.T) { } } +// TestH2H1ReqPhaseSetHeader tests mruby request phase hook +// modifies request header fields. +func TestH2H1ReqPhaseSetHeader(t *testing.T) { + st := newServerTester([]string{"--request-phase-file=" + testDir + "/req-set-header.rb"}, t, func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("User-Agent"), "mruby"; got != want { + t.Errorf("User-Agent = %v; want %v", got, want) + } + }) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1ReqPhaseSetHeader", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, 200; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestH2H1ReqPhaseReturn tests mruby request phase hook returns +// custom response. +func TestH2H1ReqPhaseReturn(t *testing.T) { + st := newServerTester([]string{"--request-phase-file=" + testDir + "/return.rb"}, t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1ReqPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, 404; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "11"}, + {"from", "mruby"}, + } + for _, tt := range hdtests { + if got, want := res.header.Get(tt.k), tt.v; got != want { + t.Errorf("%v = %v; want %v", tt.k, got, want) + } + } + + if got, want := string(res.body), "Hello World"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + +// TestH2H1RespPhaseSetHeader tests mruby response phase hook modifies +// response header fields. +func TestH2H1RespPhaseSetHeader(t *testing.T) { + st := newServerTester([]string{"--response-phase-file=" + testDir + "/resp-set-header.rb"}, t, noopHandler) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1RespPhaseSetHeader", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, 200; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + if got, want := res.header.Get("alpha"), "bravo"; got != want { + t.Errorf("alpha = %v; want %v", got, want) + } +} + +// TestH2H1RespPhaseReturn tests mruby response phase hook returns +// custom response. +func TestH2H1RespPhaseReturn(t *testing.T) { + st := newServerTester([]string{"--response-phase-file=" + testDir + "/return.rb"}, t, noopHandler) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H1RespPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, 404; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "11"}, + {"from", "mruby"}, + } + for _, tt := range hdtests { + if got, want := res.header.Get(tt.k), tt.v; got != want { + t.Errorf("%v = %v; want %v", tt.k, got, want) + } + } + + if got, want := string(res.body), "Hello World"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + // TestH2H1Upgrade tests HTTP Upgrade to HTTP/2 func TestH2H1Upgrade(t *testing.T) { st := newServerTester(nil, t, func(w http.ResponseWriter, r *http.Request) {}) @@ -875,3 +989,73 @@ func TestH2H2TLSXfp(t *testing.T) { t.Errorf("res.status: %v; want %v", got, want) } } + +// TestH2H2ReqPhaseReturn tests mruby request phase hook returns +// custom response. +func TestH2H2ReqPhaseReturn(t *testing.T) { + st := newServerTester([]string{"--http2-bridge", "--request-phase-file=" + testDir + "/return.rb"}, t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H2ReqPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, 404; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "11"}, + {"from", "mruby"}, + } + for _, tt := range hdtests { + if got, want := res.header.Get(tt.k), tt.v; got != want { + t.Errorf("%v = %v; want %v", tt.k, got, want) + } + } + + if got, want := string(res.body), "Hello World"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + +// TestH2H2RespPhaseReturn tests mruby response phase hook returns +// custom response. +func TestH2H2RespPhaseReturn(t *testing.T) { + st := newServerTester([]string{"--http2-bridge", "--response-phase-file=" + testDir + "/return.rb"}, t, noopHandler) + defer st.Close() + + res, err := st.http2(requestParam{ + name: "TestH2H2RespPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.http2() = %v", err) + } + + if got, want := res.status, 404; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "11"}, + {"from", "mruby"}, + } + for _, tt := range hdtests { + if got, want := res.header.Get(tt.k), tt.v; got != want { + t.Errorf("%v = %v; want %v", tt.k, got, want) + } + } + + if got, want := string(res.body), "Hello World"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} diff --git a/integration-tests/nghttpx_spdy_test.go b/integration-tests/nghttpx_spdy_test.go index 76d349e8..e0447fad 100644 --- a/integration-tests/nghttpx_spdy_test.go +++ b/integration-tests/nghttpx_spdy_test.go @@ -230,6 +230,120 @@ func TestS3H1InvalidMethod(t *testing.T) { } } +// TestS3H1ReqPhaseSetHeader tests mruby request phase hook +// modifies request header fields. +func TestS3H1ReqPhaseSetHeader(t *testing.T) { + st := newServerTesterTLS([]string{"--npn-list=spdy/3.1", "--request-phase-file=" + testDir + "/req-set-header.rb"}, t, func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("User-Agent"), "mruby"; got != want { + t.Errorf("User-Agent = %v; want %v", got, want) + } + }) + defer st.Close() + + res, err := st.spdy(requestParam{ + name: "TestS3H1ReqPhaseSetHeader", + }) + if err != nil { + t.Fatalf("Error st.spdy() = %v", err) + } + + if got, want := res.status, 200; got != want { + t.Errorf("status = %v; want %v", got, want) + } +} + +// TestS3H1ReqPhaseReturn tests mruby request phase hook returns +// custom response. +func TestS3H1ReqPhaseReturn(t *testing.T) { + st := newServerTesterTLS([]string{"--npn-list=spdy/3.1", "--request-phase-file=" + testDir + "/return.rb"}, t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }) + defer st.Close() + + res, err := st.spdy(requestParam{ + name: "TestS3H1ReqPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.spdy() = %v", err) + } + + if got, want := res.status, 404; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "11"}, + {"from", "mruby"}, + } + for _, tt := range hdtests { + if got, want := res.header.Get(tt.k), tt.v; got != want { + t.Errorf("%v = %v; want %v", tt.k, got, want) + } + } + + if got, want := string(res.body), "Hello World"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + +// TestS3H1RespPhaseSetHeader tests mruby response phase hook modifies +// response header fields. +func TestS3H1RespPhaseSetHeader(t *testing.T) { + st := newServerTesterTLS([]string{"--npn-list=spdy/3.1", "--response-phase-file=" + testDir + "/resp-set-header.rb"}, t, noopHandler) + defer st.Close() + + res, err := st.spdy(requestParam{ + name: "TestS3H1RespPhaseSetHeader", + }) + if err != nil { + t.Fatalf("Error st.spdy() = %v", err) + } + + if got, want := res.status, 200; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + if got, want := res.header.Get("alpha"), "bravo"; got != want { + t.Errorf("alpha = %v; want %v", got, want) + } +} + +// TestS3H1RespPhaseReturn tests mruby response phase hook returns +// custom response. +func TestS3H1RespPhaseReturn(t *testing.T) { + st := newServerTesterTLS([]string{"--npn-list=spdy/3.1", "--response-phase-file=" + testDir + "/return.rb"}, t, noopHandler) + defer st.Close() + + res, err := st.spdy(requestParam{ + name: "TestS3H1RespPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.spdy() = %v", err) + } + + if got, want := res.status, 404; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "11"}, + {"from", "mruby"}, + } + for _, tt := range hdtests { + if got, want := res.header.Get(tt.k), tt.v; got != want { + t.Errorf("%v = %v; want %v", tt.k, got, want) + } + } + + if got, want := string(res.body), "Hello World"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + // TestS3H2ConnectFailure tests that server handles the situation that // connection attempt to HTTP/2 backend failed. func TestS3H2ConnectFailure(t *testing.T) { @@ -250,3 +364,73 @@ func TestS3H2ConnectFailure(t *testing.T) { t.Errorf("status: %v; want %v", got, want) } } + +// TestS3H2ReqPhaseReturn tests mruby request phase hook returns +// custom response. +func TestS3H2ReqPhaseReturn(t *testing.T) { + st := newServerTesterTLS([]string{"--npn-list=spdy/3.1", "--http2-bridge", "--request-phase-file=" + testDir + "/return.rb"}, t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("request should not be forwarded") + }) + defer st.Close() + + res, err := st.spdy(requestParam{ + name: "TestS3H2ReqPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.spdy() = %v", err) + } + + if got, want := res.status, 404; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "11"}, + {"from", "mruby"}, + } + for _, tt := range hdtests { + if got, want := res.header.Get(tt.k), tt.v; got != want { + t.Errorf("%v = %v; want %v", tt.k, got, want) + } + } + + if got, want := string(res.body), "Hello World"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} + +// TestS3H2RespPhaseReturn tests mruby response phase hook returns +// custom response. +func TestS3H2RespPhaseReturn(t *testing.T) { + st := newServerTesterTLS([]string{"--npn-list=spdy/3.1", "--http2-bridge", "--response-phase-file=" + testDir + "/return.rb"}, t, noopHandler) + defer st.Close() + + res, err := st.spdy(requestParam{ + name: "TestS3H2RespPhaseReturn", + }) + if err != nil { + t.Fatalf("Error st.spdy() = %v", err) + } + + if got, want := res.status, 404; got != want { + t.Errorf("status = %v; want %v", got, want) + } + + hdtests := []struct { + k, v string + }{ + {"content-length", "11"}, + {"from", "mruby"}, + } + for _, tt := range hdtests { + if got, want := res.header.Get(tt.k), tt.v; got != want { + t.Errorf("%v = %v; want %v", tt.k, got, want) + } + } + + if got, want := string(res.body), "Hello World"; got != want { + t.Errorf("body = %v; want %v", got, want) + } +} diff --git a/integration-tests/req-set-header.rb b/integration-tests/req-set-header.rb new file mode 100644 index 00000000..27e9dc34 --- /dev/null +++ b/integration-tests/req-set-header.rb @@ -0,0 +1,3 @@ +Nghttpx.run do |env| + env.req.set_header "User-Agent", "mruby" +end diff --git a/integration-tests/resp-set-header.rb b/integration-tests/resp-set-header.rb new file mode 100644 index 00000000..b947ce3b --- /dev/null +++ b/integration-tests/resp-set-header.rb @@ -0,0 +1,3 @@ +Nghttpx.run do |env| + env.resp.set_header "Alpha", "bravo" +end diff --git a/integration-tests/return.rb b/integration-tests/return.rb new file mode 100644 index 00000000..907d837f --- /dev/null +++ b/integration-tests/return.rb @@ -0,0 +1,8 @@ +Nghttpx.run do |env| + resp = env.resp + + resp.clear_headers + resp.status = 404 + resp.add_header "from", "mruby" + resp.return "Hello World" +end diff --git a/makerelease.sh b/makerelease.sh index 054f6a65..60d5ab64 100755 --- a/makerelease.sh +++ b/makerelease.sh @@ -6,6 +6,7 @@ PREV_TAG=$2 git checkout refs/tags/$TAG git log --pretty=fuller --date=short refs/tags/$PREV_TAG..HEAD > ChangeLog -./configure && \ +git submodule update --init +./configure --with-mruby && \ make dist-bzip2 && make dist-gzip && make dist-xz || echo "error" make distclean diff --git a/src/Makefile.am b/src/Makefile.am index 304b6c09..e3575669 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -130,12 +130,29 @@ if HAVE_SPDYLAY NGHTTPX_SRCS += shrpx_spdy_upstream.cc shrpx_spdy_upstream.h endif # HAVE_SPDYLAY +if HAVE_MRUBY +NGHTTPX_SRCS += \ + shrpx_mruby.cc shrpx_mruby.h \ + shrpx_mruby_module.cc shrpx_mruby_module.h \ + shrpx_mruby_module_env.cc shrpx_mruby_module_env.h \ + shrpx_mruby_module_request.cc shrpx_mruby_module_request.h \ + shrpx_mruby_module_response.cc shrpx_mruby_module_response.h +endif # HAVE_MRUBY + noinst_LIBRARIES = libnghttpx.a libnghttpx_a_SOURCES = ${NGHTTPX_SRCS} +libnghttpx_a_CPPFLAGS = ${AM_CPPFLAGS} nghttpx_SOURCES = shrpx.cc shrpx.h +nghttpx_CPPFLAGS = ${libnghttpx_a_CPPFLAGS} nghttpx_LDADD = libnghttpx.a ${LDADD} +if HAVE_MRUBY +libnghttpx_a_CPPFLAGS += \ + -I${top_srcdir}/third-party/mruby/include @LIBMRUBY_CFLAGS@ +nghttpx_LDADD += -L${top_builddir}/third-party/mruby/build/lib @LIBMRUBY_LIBS@ +endif # HAVE_MRUBY + if HAVE_CUNIT check_PROGRAMS += nghttpx-unittest nghttpx_unittest_SOURCES = shrpx-unittest.cc \ @@ -148,10 +165,17 @@ nghttpx_unittest_SOURCES = shrpx-unittest.cc \ nghttp2_gzip.c nghttp2_gzip.h \ buffer_test.cc buffer_test.h \ memchunk_test.cc memchunk_test.h -nghttpx_unittest_CPPFLAGS = ${AM_CPPFLAGS}\ +nghttpx_unittest_CPPFLAGS = ${AM_CPPFLAGS} \ -DNGHTTP2_TESTS_DIR=\"$(top_srcdir)/tests\" nghttpx_unittest_LDADD = libnghttpx.a ${LDADD} @CUNIT_LIBS@ @TESTLDADD@ +if HAVE_MRUBY +nghttpx_unittest_CPPFLAGS += \ + -I${top_srcdir}/third-party/mruby/include @LIBMRUBY_CFLAGS@ +nghttpx_unittest_LDADD += \ + -L${top_builddir}/third-party/mruby/build/lib @LIBMRUBY_LIBS@ +endif # HAVE_MRUBY + TESTS += nghttpx-unittest endif # HAVE_CUNIT diff --git a/src/http2.cc b/src/http2.cc index bba0464a..8059ac04 100644 --- a/src/http2.cc +++ b/src/http2.cc @@ -664,6 +664,15 @@ const Headers::value_type *get_header(const HeaderIndex &hdidx, int16_t token, return &nva[i]; } +Headers::value_type *get_header(const HeaderIndex &hdidx, int16_t token, + Headers &nva) { + auto i = hdidx[token]; + if (i == -1) { + return nullptr; + } + return &nva[i]; +} + namespace { template InputIt skip_lws(InputIt first, InputIt last) { for (; first != last; ++first) { @@ -1299,6 +1308,91 @@ const char *to_method_string(int method_token) { return http_method_str(static_cast(method_token)); } +int get_pure_path_component(const char **base, size_t *baselen, + const std::string &uri) { + int rv; + + http_parser_url u{}; + rv = http_parser_parse_url(uri.c_str(), uri.size(), 0, &u); + if (rv != 0) { + return -1; + } + + if (u.field_set & (1 << UF_PATH)) { + auto &f = u.field_data[UF_PATH]; + *base = uri.c_str() + f.off; + *baselen = f.len; + + return 0; + } + + *base = "/"; + *baselen = 1; + + return 0; +} + +int construct_push_component(std::string &scheme, std::string &authority, + std::string &path, const char *base, + size_t baselen, const char *uri, size_t len) { + int rv; + const char *rel, *relq = nullptr; + size_t rellen, relqlen = 0; + + http_parser_url u{}; + + rv = http_parser_parse_url(uri, len, 0, &u); + + if (rv != 0) { + if (uri[0] == '/') { + return -1; + } + + // treat link_url as relative URI. + auto end = std::find(uri, uri + len, '#'); + auto q = std::find(uri, end, '?'); + + rel = uri; + rellen = q - uri; + if (q != end) { + relq = q + 1; + relqlen = end - relq; + } + } else { + if (u.field_set & (1 << UF_SCHEMA)) { + http2::copy_url_component(scheme, &u, UF_SCHEMA, uri); + } + + if (u.field_set & (1 << UF_HOST)) { + http2::copy_url_component(authority, &u, UF_HOST, uri); + if (u.field_set & (1 << UF_PORT)) { + authority += ":"; + authority += util::utos(u.port); + } + } + + if (u.field_set & (1 << UF_PATH)) { + auto &f = u.field_data[UF_PATH]; + rel = uri + f.off; + rellen = f.len; + } else { + rel = "/"; + rellen = 1; + } + + if (u.field_set & (1 << UF_QUERY)) { + auto &f = u.field_data[UF_QUERY]; + relq = uri + f.off; + relqlen = f.len; + } + } + + path = + http2::path_join(base, baselen, nullptr, 0, rel, rellen, relq, relqlen); + + return 0; +} + } // namespace http2 } // namespace nghttp2 diff --git a/src/http2.h b/src/http2.h index 182c45d9..8fd32f85 100644 --- a/src/http2.h +++ b/src/http2.h @@ -262,6 +262,9 @@ bool http2_mandatory_request_headers_presence(const HeaderIndex &hdidx); const Headers::value_type *get_header(const HeaderIndex &hdidx, int16_t token, const Headers &nva); +Headers::value_type *get_header(const HeaderIndex &hdidx, int16_t token, + Headers &nva); + struct LinkHeader { // The region of URI is [uri.first, uri.second). std::pair uri; @@ -349,6 +352,21 @@ std::string rewrite_clean_path(InputIt first, InputIt last) { return path; } +// Stores path component of |uri| in *base. Its extracted length is +// stored in *baselen. The extracted path does not include query +// component. This function returns 0 if it succeeds, or -1. +int get_pure_path_component(const char **base, size_t *baselen, + const std::string &uri); + +// Deduces scheme, authority and path from given |uri| of length +// |len|, and stores them in |scheme|, |authority|, and |path| +// respectively. If |uri| is relative path, path resolution is taken +// palce using path given in |base| of length |baselen|. This +// function returns 0 if it succeeds, or -1. +int construct_push_component(std::string &scheme, std::string &authority, + std::string &path, const char *base, + size_t baselen, const char *uri, size_t len); + } // namespace http2 } // namespace nghttp2 diff --git a/src/http2_test.cc b/src/http2_test.cc index 9a99a3a9..cda2a04e 100644 --- a/src/http2_test.cc +++ b/src/http2_test.cc @@ -880,4 +880,104 @@ void test_http2_rewrite_clean_path(void) { CU_ASSERT(src == http2::rewrite_clean_path(std::begin(src), std::end(src))); } +void test_http2_get_pure_path_component(void) { + const char *base; + size_t len; + std::string path; + + path = "/"; + CU_ASSERT(0 == http2::get_pure_path_component(&base, &len, path)); + CU_ASSERT(util::streq_l("/", base, len)); + + path = "/foo"; + CU_ASSERT(0 == http2::get_pure_path_component(&base, &len, path)); + CU_ASSERT(util::streq_l("/foo", base, len)); + + path = "https://example.org/bar"; + CU_ASSERT(0 == http2::get_pure_path_component(&base, &len, path)); + CU_ASSERT(util::streq_l("/bar", base, len)); + + path = "https://example.org/alpha?q=a"; + CU_ASSERT(0 == http2::get_pure_path_component(&base, &len, path)); + CU_ASSERT(util::streq_l("/alpha", base, len)); + + path = "https://example.org/bravo?q=a#fragment"; + CU_ASSERT(0 == http2::get_pure_path_component(&base, &len, path)); + CU_ASSERT(util::streq_l("/bravo", base, len)); + + path = "\x01\x02"; + CU_ASSERT(-1 == http2::get_pure_path_component(&base, &len, path)); +} + +void test_http2_construct_push_component(void) { + const char *base; + size_t baselen; + std::string uri; + std::string scheme, authority, path; + + base = "/b/"; + baselen = 3; + + uri = "https://example.org/foo"; + + CU_ASSERT(0 == http2::construct_push_component(scheme, authority, path, base, + baselen, uri.c_str(), + uri.size())); + CU_ASSERT("https" == scheme); + CU_ASSERT("example.org" == authority); + CU_ASSERT("/foo" == path); + + scheme.clear(); + authority.clear(); + path.clear(); + + uri = "/foo/bar?q=a"; + + CU_ASSERT(0 == http2::construct_push_component(scheme, authority, path, base, + baselen, uri.c_str(), + uri.size())); + CU_ASSERT("" == scheme); + CU_ASSERT("" == authority); + CU_ASSERT("/foo/bar?q=a" == path); + + scheme.clear(); + authority.clear(); + path.clear(); + + uri = "foo/../bar?q=a"; + + CU_ASSERT(0 == http2::construct_push_component(scheme, authority, path, base, + baselen, uri.c_str(), + uri.size())); + CU_ASSERT("" == scheme); + CU_ASSERT("" == authority); + CU_ASSERT("/b/bar?q=a" == path); + + scheme.clear(); + authority.clear(); + path.clear(); + + uri = ""; + + CU_ASSERT(0 == http2::construct_push_component(scheme, authority, path, base, + baselen, uri.c_str(), + uri.size())); + CU_ASSERT("" == scheme); + CU_ASSERT("" == authority); + CU_ASSERT("/" == path); + + scheme.clear(); + authority.clear(); + path.clear(); + + uri = "?q=a"; + + CU_ASSERT(0 == http2::construct_push_component(scheme, authority, path, base, + baselen, uri.c_str(), + uri.size())); + CU_ASSERT("" == scheme); + CU_ASSERT("" == authority); + CU_ASSERT("/b/?q=a" == path); +} + } // namespace shrpx diff --git a/src/http2_test.h b/src/http2_test.h index bc65d453..80f14cd8 100644 --- a/src/http2_test.h +++ b/src/http2_test.h @@ -47,6 +47,8 @@ void test_http2_parse_link_header(void); void test_http2_path_join(void); void test_http2_normalize_path(void); void test_http2_rewrite_clean_path(void); +void test_http2_get_pure_path_component(void); +void test_http2_construct_push_component(void); } // namespace shrpx diff --git a/src/shrpx-unittest.cc b/src/shrpx-unittest.cc index 10eefb8b..03219a65 100644 --- a/src/shrpx-unittest.cc +++ b/src/shrpx-unittest.cc @@ -100,6 +100,10 @@ int main(int argc, char *argv[]) { shrpx::test_http2_normalize_path) || !CU_add_test(pSuite, "http2_rewrite_clean_path", shrpx::test_http2_rewrite_clean_path) || + !CU_add_test(pSuite, "http2_get_pure_path_component", + shrpx::test_http2_get_pure_path_component) || + !CU_add_test(pSuite, "http2_construct_push_component", + shrpx::test_http2_construct_push_component) || !CU_add_test(pSuite, "downstream_index_request_headers", shrpx::test_downstream_index_request_headers) || !CU_add_test(pSuite, "downstream_index_response_headers", diff --git a/src/shrpx.cc b/src/shrpx.cc index 17c01f80..7ef8e1ed 100644 --- a/src/shrpx.cc +++ b/src/shrpx.cc @@ -926,9 +926,13 @@ int event_loop() { #endif // !NOTHREADS if (get_config()->num_worker == 1) { - conn_handler->create_single_worker(); + rv = conn_handler->create_single_worker(); } else { - conn_handler->create_worker_thread(get_config()->num_worker); + rv = conn_handler->create_worker_thread(get_config()->num_worker); + } + + if (rv != 0) { + return -1; } #ifndef NOTHREADS @@ -1726,6 +1730,16 @@ Process: Run this program as . This option is intended to be used to drop root privileges. +Scripting: + --request-phase-file= + Set mruby script file which will be executed when + request header fields are completely received from + frontend. This hook is called request phase hook. + --response-phase-file= + Set mruby script file which will be executed when + response header fields are completely received from + backend. This hook is called response phase hook. + Misc: --conf= Load configuration from . @@ -1899,6 +1913,8 @@ int main(int argc, char **argv) { 89}, {SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_MAX_FAIL, required_argument, &flag, 90}, + {SHRPX_OPT_REQUEST_PHASE_FILE, required_argument, &flag, 91}, + {SHRPX_OPT_RESPONSE_PHASE_FILE, required_argument, &flag, 92}, {nullptr, 0, nullptr, 0}}; int option_index = 0; @@ -2296,6 +2312,13 @@ int main(int argc, char **argv) { cmdcfgs.emplace_back(SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_MAX_FAIL, optarg); break; + case 91: + // --request-phase-file + cmdcfgs.emplace_back(SHRPX_OPT_REQUEST_PHASE_FILE, optarg); + break; + case 92: + // --response-phase-file + cmdcfgs.emplace_back(SHRPX_OPT_RESPONSE_PHASE_FILE, optarg); default: break; } @@ -2613,7 +2636,9 @@ int main(int argc, char **argv) { act.sa_handler = SIG_IGN; sigaction(SIGPIPE, &act, nullptr); - event_loop(); + if (event_loop() != 0) { + return -1; + } LOG(NOTICE) << "Shutdown momentarily"; diff --git a/src/shrpx_client_handler.cc b/src/shrpx_client_handler.cc index 37293a5e..87c1659e 100644 --- a/src/shrpx_client_handler.cc +++ b/src/shrpx_client_handler.cc @@ -739,41 +739,23 @@ namespace { // HttpDownstreamConnection::push_request_headers(), but vastly // simplified since we only care about absolute URI. std::string construct_absolute_request_uri(Downstream *downstream) { - const char *authority = nullptr, *host = nullptr; - if (!downstream->get_request_http2_authority().empty()) { - authority = downstream->get_request_http2_authority().c_str(); - } - auto h = downstream->get_request_header(http2::HD_HOST); - if (h) { - host = h->value.c_str(); - } - if (!authority && !host) { + auto &authority = downstream->get_request_http2_authority(); + if (authority.empty()) { return downstream->get_request_path(); } std::string uri; - if (downstream->get_request_http2_scheme().empty()) { + auto &scheme = downstream->get_request_http2_scheme(); + if (scheme.empty()) { // We may have to log the request which lacks scheme (e.g., // http/1.1 with origin form). uri += "http://"; } else { - uri += downstream->get_request_http2_scheme(); + uri += scheme; uri += "://"; } - if (authority) { - uri += authority; - } else { - uri += host; - } + uri += authority; + uri += downstream->get_request_path(); - // Server-wide OPTIONS takes following form in proxy request: - // - // OPTIONS http://example.org HTTP/1.1 - // - // Notice that no slash after authority. See - // http://tools.ietf.org/html/rfc7230#section-5.3.4 - if (downstream->get_request_path() != "*") { - uri += downstream->get_request_path(); - } return uri; } } // namespace @@ -787,12 +769,15 @@ void ClientHandler::write_accesslog(Downstream *downstream) { downstream, ipaddr_.c_str(), http2::to_method_string(downstream->get_request_method()), - (downstream->get_request_method() != HTTP_CONNECT && - (get_config()->http2_proxy || get_config()->client_proxy)) - ? construct_absolute_request_uri(downstream).c_str() - : downstream->get_request_path().empty() - ? downstream->get_request_http2_authority().c_str() - : downstream->get_request_path().c_str(), + downstream->get_request_method() == HTTP_CONNECT + ? downstream->get_request_http2_authority().c_str() + : (get_config()->http2_proxy || get_config()->client_proxy) + ? construct_absolute_request_uri(downstream).c_str() + : downstream->get_request_path().empty() + ? downstream->get_request_method() == HTTP_OPTIONS + ? "*" + : "-" + : downstream->get_request_path().c_str(), alpn_.c_str(), nghttp2::ssl::get_tls_session_info(&tls_info, conn_.tls.ssl), diff --git a/src/shrpx_config.cc b/src/shrpx_config.cc index 45db8daf..09ee2e36 100644 --- a/src/shrpx_config.cc +++ b/src/shrpx_config.cc @@ -696,6 +696,8 @@ enum { SHRPX_OPTID_PRIVATE_KEY_PASSWD_FILE, SHRPX_OPTID_READ_BURST, SHRPX_OPTID_READ_RATE, + SHRPX_OPTID_REQUEST_PHASE_FILE, + SHRPX_OPTID_RESPONSE_PHASE_FILE, SHRPX_OPTID_RLIMIT_NOFILE, SHRPX_OPTID_STREAM_READ_TIMEOUT, SHRPX_OPTID_STREAM_WRITE_TIMEOUT, @@ -1017,6 +1019,11 @@ int option_lookup_token(const char *name, size_t namelen) { break; case 18: switch (name[17]) { + case 'e': + if (util::strieq_l("request-phase-fil", name, 17)) { + return SHRPX_OPTID_REQUEST_PHASE_FILE; + } + break; case 'r': if (util::strieq_l("add-request-heade", name, 17)) { return SHRPX_OPTID_ADD_REQUEST_HEADER; @@ -1035,6 +1042,9 @@ int option_lookup_token(const char *name, size_t namelen) { if (util::strieq_l("no-location-rewrit", name, 18)) { return SHRPX_OPTID_NO_LOCATION_REWRITE; } + if (util::strieq_l("response-phase-fil", name, 18)) { + return SHRPX_OPTID_RESPONSE_PHASE_FILE; + } if (util::strieq_l("tls-ticket-key-fil", name, 18)) { return SHRPX_OPTID_TLS_TICKET_KEY_FILE; } @@ -1938,6 +1948,14 @@ int parse_config(const char *opt, const char *optarg, case SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_MAX_FAIL: return parse_uint(&mod_config()->tls_ticket_key_memcached_max_fail, opt, optarg); + case SHRPX_OPTID_REQUEST_PHASE_FILE: + mod_config()->request_phase_file = strcopy(optarg); + + return 0; + case SHRPX_OPTID_RESPONSE_PHASE_FILE: + mod_config()->response_phase_file = strcopy(optarg); + + return 0; case SHRPX_OPTID_CONF: LOG(WARN) << "conf: ignored"; diff --git a/src/shrpx_config.h b/src/shrpx_config.h index e75a859c..2bbb9647 100644 --- a/src/shrpx_config.h +++ b/src/shrpx_config.h @@ -183,6 +183,8 @@ constexpr char SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_MAX_RETRY[] = "tls-ticket-key-memcached-max-retry"; constexpr char SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_MAX_FAIL[] = "tls-ticket-key-memcached-max-fail"; +constexpr char SHRPX_OPT_REQUEST_PHASE_FILE[] = "request-phase-file"; +constexpr char SHRPX_OPT_RESPONSE_PHASE_FILE[] = "response-phase-file"; union sockaddr_union { sockaddr_storage storage; @@ -314,6 +316,8 @@ struct Config { std::unique_ptr user; std::unique_ptr session_cache_memcached_host; std::unique_ptr tls_ticket_key_memcached_host; + std::unique_ptr request_phase_file; + std::unique_ptr response_phase_file; FILE *http2_upstream_dump_request_header; FILE *http2_upstream_dump_response_header; nghttp2_session_callbacks *http2_upstream_callbacks; diff --git a/src/shrpx_connection_handler.cc b/src/shrpx_connection_handler.cc index 3d9cf070..bce7e0db 100644 --- a/src/shrpx_connection_handler.cc +++ b/src/shrpx_connection_handler.cc @@ -156,7 +156,7 @@ void ConnectionHandler::worker_reopen_log_files() { } } -void ConnectionHandler::create_single_worker() { +int ConnectionHandler::create_single_worker() { auto cert_tree = ssl::create_cert_lookup_tree(); auto sv_ssl_ctx = ssl::setup_server_ssl_context(all_ssl_ctx_, cert_tree); auto cl_ssl_ctx = ssl::setup_client_ssl_context(); @@ -167,9 +167,16 @@ void ConnectionHandler::create_single_worker() { single_worker_ = make_unique(loop_, sv_ssl_ctx, cl_ssl_ctx, cert_tree, ticket_keys_); +#ifdef HAVE_MRUBY + if (single_worker_->create_mruby_context() != 0) { + return -1; + } +#endif // HAVE_MRUBY + + return 0; } -void ConnectionHandler::create_worker_thread(size_t num) { +int ConnectionHandler::create_worker_thread(size_t num) { #ifndef NOTHREADS assert(workers_.size() == 0); @@ -186,7 +193,12 @@ void ConnectionHandler::create_worker_thread(size_t num) { auto worker = make_unique(loop, sv_ssl_ctx, cl_ssl_ctx, cert_tree, ticket_keys_); - worker->run_async(); +#ifdef HAVE_MRUBY + if (worker->create_mruby_context() != 0) { + return -1; + } +#endif // HAVE_MRUBY + workers_.push_back(std::move(worker)); worker_loops_.push_back(loop); @@ -194,7 +206,13 @@ void ConnectionHandler::create_worker_thread(size_t num) { LLOG(INFO, this) << "Created thread #" << workers_.size() - 1; } } + + for (auto &worker : workers_) { + worker->run_async(); + } #endif // NOTHREADS + + return 0; } void ConnectionHandler::join_worker() { diff --git a/src/shrpx_connection_handler.h b/src/shrpx_connection_handler.h index 44fe5955..8ce25eec 100644 --- a/src/shrpx_connection_handler.h +++ b/src/shrpx_connection_handler.h @@ -73,10 +73,10 @@ public: ~ConnectionHandler(); int handle_connection(int fd, sockaddr *addr, int addrlen); // Creates Worker object for single threaded configuration. - void create_single_worker(); + int create_single_worker(); // Creates |num| Worker objects for multi threaded configuration. // The |num| must be strictly more than 1. - void create_worker_thread(size_t num); + int create_worker_thread(size_t num); void set_ticket_keys_to_worker(const std::shared_ptr &ticket_keys); void worker_reopen_log_files(); diff --git a/src/shrpx_downstream.cc b/src/shrpx_downstream.cc index a37c7f2f..eefa99f2 100644 --- a/src/shrpx_downstream.cc +++ b/src/shrpx_downstream.cc @@ -34,6 +34,11 @@ #include "shrpx_error.h" #include "shrpx_downstream_connection.h" #include "shrpx_downstream_queue.h" +#include "shrpx_worker.h" +#include "shrpx_http2_session.h" +#ifdef HAVE_MRUBY +#include "shrpx_mruby.h" +#endif // HAVE_MRUBY #include "util.h" #include "http2.h" @@ -160,6 +165,14 @@ Downstream::~Downstream() { ev_timer_stop(loop, &upstream_wtimer_); ev_timer_stop(loop, &downstream_rtimer_); ev_timer_stop(loop, &downstream_wtimer_); + +#ifdef HAVE_MRUBY + auto handler = upstream_->get_client_handler(); + auto worker = handler->get_worker(); + auto mruby_ctx = worker->get_mruby_context(); + + mruby_ctx->delete_downstream(this); +#endif // HAVE_MRUBY } // DownstreamConnection may refer to this object. Delete it now @@ -240,6 +253,8 @@ const Headers &Downstream::get_request_headers() const { return request_headers_; } +Headers &Downstream::get_request_headers() { return request_headers_; } + void Downstream::assemble_request_cookie() { std::string &cookie = assembled_request_cookie_; cookie = ""; @@ -336,6 +351,9 @@ void set_last_header_value(bool &key_prev, size_t &sum, Headers &headers, namespace { int index_headers(http2::HeaderIndex &hdidx, Headers &headers, int64_t &content_length) { + http2::init_hdidx(hdidx); + content_length = -1; + for (size_t i = 0; i < headers.size(); ++i) { auto &kv = headers[i]; util::inp_strlower(kv.name); @@ -510,6 +528,10 @@ void Downstream::set_request_http2_authority(std::string authority) { request_http2_authority_ = std::move(authority); } +void Downstream::append_request_http2_authority(const char *data, size_t len) { + request_http2_authority_.append(data, len); +} + void Downstream::set_request_major(int major) { request_major_ = major; } void Downstream::set_request_minor(int minor) { request_minor_ = minor; } @@ -604,6 +626,8 @@ const Headers &Downstream::get_response_headers() const { return response_headers_; } +Headers &Downstream::get_response_headers() { return response_headers_; } + int Downstream::index_response_headers() { return index_headers(response_hdidx_, response_headers_, response_content_length_); @@ -614,6 +638,10 @@ Downstream::get_response_header(int16_t token) const { return http2::get_header(response_hdidx_, token, response_headers_); } +Headers::value_type *Downstream::get_response_header(int16_t token) { + return http2::get_header(response_hdidx_, token, response_headers_); +} + void Downstream::rewrite_location_response_header( const std::string &upstream_scheme) { auto hd = diff --git a/src/shrpx_downstream.h b/src/shrpx_downstream.h index c64aba4f..f90e80ce 100644 --- a/src/shrpx_downstream.h +++ b/src/shrpx_downstream.h @@ -96,6 +96,7 @@ public: const std::string &get_http2_settings() const; // downstream request API const Headers &get_request_headers() const; + Headers &get_request_headers(); // Crumbles (split cookie by ";") in request_headers_ and returns // them. Headers::no_index is inherited. Headers crumble_request_cookie(); @@ -149,15 +150,19 @@ public: get_request_start_time() const; void append_request_path(const char *data, size_t len); // Returns request path. For HTTP/1.1, this is request-target. For - // HTTP/2, this is :path header field value. + // HTTP/2, this is :path header field value. For CONNECT request, + // this is empty. const std::string &get_request_path() const; // Returns HTTP/2 :scheme header field value. const std::string &get_request_http2_scheme() const; void set_request_http2_scheme(std::string scheme); - // Returns HTTP/2 :authority header field value. We also set the - // value retrieved from absolute-form HTTP/1 request. + // Returns :authority or host header field value. We may deduce it + // from absolute-form HTTP/1 request. We also store authority-form + // HTTP/1 request. This could be empty if request comes from + // HTTP/1.0 without Host header field and origin-form. const std::string &get_request_http2_authority() const; void set_request_http2_authority(std::string authority); + void append_request_http2_authority(const char *data, size_t len); void set_request_major(int major); void set_request_minor(int minor); int get_request_major() const; @@ -207,6 +212,7 @@ public: bool request_submission_ready() const; // downstream response API const Headers &get_response_headers() const; + Headers &get_response_headers(); // Lower the response header field names and indexes response // headers. If there are invalid headers (e.g., multiple // Content-Length with different values), returns -1. @@ -216,6 +222,7 @@ public: // the beginning. If no such header is found, returns nullptr. // This function must be called after response headers are indexed. const Headers::value_type *get_response_header(int16_t token) const; + Headers::value_type *get_response_header(int16_t token); // Rewrites the location response header field. void rewrite_location_response_header(const std::string &upstream_scheme); void add_response_header(std::string name, std::string value); diff --git a/src/shrpx_error.h b/src/shrpx_error.h index f1d1bffa..5cb8490c 100644 --- a/src/shrpx_error.h +++ b/src/shrpx_error.h @@ -36,6 +36,7 @@ enum ErrorCode { SHRPX_ERR_NETWORK = -100, SHRPX_ERR_EOF = -101, SHRPX_ERR_INPROGRESS = -102, + SHRPX_ERR_DCONN_CANCELED = -103, }; } // namespace shrpx diff --git a/src/shrpx_http2_downstream_connection.cc b/src/shrpx_http2_downstream_connection.cc index f4377b44..86e200f6 100644 --- a/src/shrpx_http2_downstream_connection.cc +++ b/src/shrpx_http2_downstream_connection.cc @@ -251,10 +251,10 @@ int Http2DownstreamConnection::push_request_headers() { downstream_->set_request_pending(false); + auto method = downstream_->get_request_method(); auto no_host_rewrite = get_config()->no_host_rewrite || get_config()->http2_proxy || - get_config()->client_proxy || - downstream_->get_request_method() == HTTP_CONNECT; + get_config()->client_proxy || method == HTTP_CONNECT; // http2session_ has already in CONNECTED state, so we can get // addr_idx here. @@ -265,37 +265,21 @@ int Http2DownstreamConnection::push_request_headers() { .addrs[addr_idx] .hostport.get(); - const char *authority = nullptr, *host = nullptr; - if (!no_host_rewrite) { - if (!downstream_->get_request_http2_authority().empty()) { - authority = downstream_hostport; - } - if (downstream_->get_request_header(http2::HD_HOST)) { - host = downstream_hostport; - } - } else { - if (!downstream_->get_request_http2_authority().empty()) { - authority = downstream_->get_request_http2_authority().c_str(); - } - auto h = downstream_->get_request_header(http2::HD_HOST); - if (h) { - host = h->value.c_str(); - } + // For HTTP/1.0 request, there is no authority in request. In that + // case, we use backend server's host nonetheless. + const char *authority = downstream_hostport; + auto &req_authority = downstream_->get_request_http2_authority(); + if (no_host_rewrite && !req_authority.empty()) { + authority = req_authority.c_str(); } - if (!authority && !host) { - // upstream is HTTP/1.0. We use backend server's host - // nonetheless. - host = downstream_hostport; + if (!authority) { + authority = downstream_hostport; } - if (authority) { - downstream_->set_request_downstream_host(authority); - } else { - downstream_->set_request_downstream_host(host); - } + downstream_->set_request_downstream_host(authority); - size_t nheader = downstream_->get_request_headers().size(); + auto nheader = downstream_->get_request_headers().size(); Headers cookies; if (!get_config()->http2_no_cookie_crumbling) { @@ -306,7 +290,7 @@ int Http2DownstreamConnection::push_request_headers() { // 1. :method // 2. :scheme // 3. :path - // 4. :authority or host (at least either of them exists) + // 4. :authority // 5. via (optional) // 6. x-forwarded-for (optional) // 7. x-forwarded-proto (optional) @@ -315,33 +299,23 @@ int Http2DownstreamConnection::push_request_headers() { nva.reserve(nheader + 8 + cookies.size() + get_config()->add_request_headers.size()); - nva.push_back(http2::make_nv_lc( - ":method", http2::to_method_string(downstream_->get_request_method()))); + nva.push_back(http2::make_nv_lc(":method", http2::to_method_string(method))); auto &scheme = downstream_->get_request_http2_scheme(); - if (downstream_->get_request_method() == HTTP_CONNECT) { - if (authority) { - nva.push_back(http2::make_nv_lc(":authority", authority)); - } else { - nva.push_back( - http2::make_nv_ls(":authority", downstream_->get_request_path())); - } - } else { + nva.push_back(http2::make_nv_lc(":authority", authority)); + + if (method != HTTP_CONNECT) { assert(!scheme.empty()); + nva.push_back(http2::make_nv_ls(":scheme", scheme)); - if (authority) { - nva.push_back(http2::make_nv_lc(":authority", authority)); + auto &path = downstream_->get_request_path(); + if (method == HTTP_OPTIONS && path.empty()) { + nva.push_back(http2::make_nv_ll(":path", "*")); + } else { + nva.push_back(http2::make_nv_ls(":path", path)); } - - nva.push_back(http2::make_nv_ls(":path", downstream_->get_request_path())); - } - - // only emit host header field if :authority is not emitted. They - // both must be the same value. - if (!authority && host) { - nva.push_back(http2::make_nv_lc("host", host)); } http2::copy_headers_to_nva(nva, downstream_->get_request_headers()); diff --git a/src/shrpx_http2_session.cc b/src/shrpx_http2_session.cc index ad13531b..6c8bb8b5 100644 --- a/src/shrpx_http2_session.cc +++ b/src/shrpx_http2_session.cc @@ -903,9 +903,15 @@ int on_response_headers(Http2Session *http2session, Downstream *downstream, rv = upstream->on_downstream_header_complete(downstream); if (rv != 0) { - http2session->submit_rst_stream(frame->hd.stream_id, - NGHTTP2_PROTOCOL_ERROR); - downstream->set_response_state(Downstream::MSG_RESET); + // Handling early return (in other words, response was hijacked by + // mruby scripting). + if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { + http2session->submit_rst_stream(frame->hd.stream_id, NGHTTP2_CANCEL); + } else { + http2session->submit_rst_stream(frame->hd.stream_id, + NGHTTP2_INTERNAL_ERROR); + downstream->set_response_state(Downstream::MSG_RESET); + } } return 0; diff --git a/src/shrpx_http2_upstream.cc b/src/shrpx_http2_upstream.cc index 26f05fe0..8a3160f5 100644 --- a/src/shrpx_http2_upstream.cc +++ b/src/shrpx_http2_upstream.cc @@ -37,6 +37,9 @@ #include "shrpx_http.h" #include "shrpx_worker.h" #include "shrpx_http2_session.h" +#ifdef HAVE_MRUBY +#include "shrpx_mruby.h" +#endif // HAVE_MRUBY #include "http2.h" #include "util.h" #include "base64.h" @@ -291,9 +294,16 @@ int Http2Upstream::on_request_headers(Downstream *downstream, downstream->set_request_method(method_token); downstream->set_request_http2_scheme(http2::value_to_str(scheme)); + // nghttp2 library guarantees either :authority or host exist + if (!authority) { + authority = downstream->get_request_header(http2::HD_HOST); + } downstream->set_request_http2_authority(http2::value_to_str(authority)); + if (path) { - if (get_config()->http2_proxy || get_config()->client_proxy) { + if (method_token == HTTP_OPTIONS && path->value == "*") { + // Server-wide OPTIONS request. Path is empty. + } else if (get_config()->http2_proxy || get_config()->client_proxy) { downstream->set_request_path(http2::value_to_str(path)); } else { auto &value = path->value; @@ -309,12 +319,31 @@ int Http2Upstream::on_request_headers(Downstream *downstream, downstream->inspect_http2_request(); downstream->set_request_state(Downstream::HEADER_COMPLETE); + +#ifdef HAVE_MRUBY + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + auto worker = handler->get_worker(); + auto mruby_ctx = worker->get_mruby_context(); + + if (mruby_ctx->run_on_request_proc(downstream) != 0) { + if (error_reply(downstream, 500) != 0) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + return 0; + } +#endif // HAVE_MRUBY + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { downstream->disable_upstream_rtimer(); downstream->set_request_state(Downstream::MSG_COMPLETE); } + if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { + return 0; + } + start_downstream(downstream); return 0; @@ -558,6 +587,20 @@ int on_frame_send_callback(nghttp2_session *session, const nghttp2_frame *frame, // downstream is in pending queue. auto ptr = downstream.get(); upstream->add_pending_downstream(std::move(downstream)); + +#ifdef HAVE_MRUBY + auto worker = handler->get_worker(); + auto mruby_ctx = worker->get_mruby_context(); + + if (mruby_ctx->run_on_request_proc(ptr) != 0) { + if (upstream->error_reply(ptr, 500) != 0) { + upstream->rst_stream(ptr, NGHTTP2_INTERNAL_ERROR); + return 0; + } + return 0; + } +#endif // HAVE_MRUBY + upstream->start_downstream(ptr); return 0; @@ -898,6 +941,11 @@ int Http2Upstream::downstream_read(DownstreamConnection *dconn) { if (rv == SHRPX_ERR_EOF) { return downstream_eof(dconn); } + if (rv == SHRPX_ERR_DCONN_CANCELED) { + downstream->pop_downstream_connection(); + handler_->signal_write(); + return 0; + } if (rv != 0) { if (rv != SHRPX_ERR_NETWORK) { if (LOG_ENABLED(INFO)) { @@ -1120,6 +1168,63 @@ ssize_t downstream_data_read_callback(nghttp2_session *session, } } // namespace +int Http2Upstream::send_reply(Downstream *downstream, const uint8_t *body, + size_t bodylen) { + int rv; + + nghttp2_data_provider data_prd, *data_prd_ptr = nullptr; + + if (bodylen) { + data_prd.source.ptr = downstream; + data_prd.read_callback = downstream_data_read_callback; + data_prd_ptr = &data_prd; + } + + auto status_code_str = util::utos(downstream->get_response_http_status()); + auto &headers = downstream->get_response_headers(); + auto nva = std::vector(); + // 2 for :status and server + nva.reserve(2 + headers.size()); + + nva.push_back(http2::make_nv_ls(":status", status_code_str)); + + for (auto &kv : headers) { + if (kv.name.empty() || kv.name[0] == ':') { + continue; + } + switch (kv.token) { + case http2::HD_CONNECTION: + case http2::HD_KEEP_ALIVE: + case http2::HD_PROXY_CONNECTION: + case http2::HD_TE: + case http2::HD_TRANSFER_ENCODING: + case http2::HD_UPGRADE: + continue; + } + nva.push_back(http2::make_nv(kv.name, kv.value, kv.no_index)); + } + + if (!downstream->get_response_header(http2::HD_SERVER)) { + nva.push_back(http2::make_nv_lc("server", get_config()->server_name)); + } + + rv = nghttp2_submit_response(session_, downstream->get_stream_id(), + nva.data(), nva.size(), data_prd_ptr); + if (nghttp2_is_fatal(rv)) { + ULOG(FATAL, this) << "nghttp2_submit_response() failed: " + << nghttp2_strerror(rv); + return -1; + } + + auto buf = downstream->get_response_buf(); + + buf->append(body, bodylen); + + downstream->set_response_state(Downstream::MSG_COMPLETE); + + return 0; +} + int Http2Upstream::error_reply(Downstream *downstream, unsigned int status_code) { int rv; @@ -1191,6 +1296,24 @@ int Http2Upstream::on_downstream_header_complete(Downstream *downstream) { downstream->get_request_http2_scheme()); } +#ifdef HAVE_MRUBY + if (!downstream->get_non_final_response()) { + auto worker = handler_->get_worker(); + auto mruby_ctx = worker->get_mruby_context(); + + if (mruby_ctx->run_on_response_proc(downstream) != 0) { + if (error_reply(downstream, 500) != 0) { + return -1; + } + return -1; + } + + if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { + return -1; + } + } +#endif // HAVE_MRUBY + size_t nheader = downstream->get_response_headers().size(); auto nva = std::vector(); // 3 means :status and possible server and via header field. @@ -1272,12 +1395,9 @@ int Http2Upstream::on_downstream_header_complete(Downstream *downstream) { // We need some conditions that must be fulfilled to initiate server // push. // - // * Server push is disabled for http2 proxy, since incoming headers - // are mixed origins. We don't know how to reliably determine the - // authority yet. - // - // * If downstream is http/2, it is likely that PUSH_PROMISE is - // coming from there, so we don't initiate PUSH_RPOMISE here. + // * Server push is disabled for http2 proxy or client proxy, since + // incoming headers are mixed origins. We don't know how to + // reliably determine the authority yet. // // * We need 200 response code for associated resource. This is too // restrictive, we will review this later. @@ -1288,8 +1408,8 @@ int Http2Upstream::on_downstream_header_complete(Downstream *downstream) { if (!get_config()->no_server_push && nghttp2_session_get_remote_settings(session_, NGHTTP2_SETTINGS_ENABLE_PUSH) == 1 && - get_config()->downstream_proto == PROTO_HTTP && - !get_config()->http2_proxy && (downstream->get_stream_id() % 2) && + !get_config()->http2_proxy && !get_config()->client_proxy && + (downstream->get_stream_id() % 2) && downstream->get_response_header(http2::HD_LINK) && downstream->get_response_http_status() == 200 && (downstream->get_request_method() == HTTP_GET || @@ -1473,80 +1593,31 @@ int Http2Upstream::on_downstream_reset(bool no_retry) { int Http2Upstream::prepare_push_promise(Downstream *downstream) { int rv; - http_parser_url u{}; - rv = http_parser_parse_url(downstream->get_request_path().c_str(), - downstream->get_request_path().size(), 0, &u); + const char *base; + size_t baselen; + + rv = http2::get_pure_path_component(&base, &baselen, + downstream->get_request_path()); if (rv != 0) { return 0; } - const char *base; - size_t baselen; - if (u.field_set & (1 << UF_PATH)) { - auto &f = u.field_data[UF_PATH]; - base = downstream->get_request_path().c_str() + f.off; - baselen = f.len; - } else { - base = "/"; - baselen = 1; - } + for (auto &kv : downstream->get_response_headers()) { if (kv.token != http2::HD_LINK) { continue; } for (auto &link : http2::parse_link_header(kv.value.c_str(), kv.value.size())) { - auto link_url = link.uri.first; - auto link_urllen = link.uri.second - link.uri.first; - const char *rel; - size_t rellen; - const char *relq = nullptr; - size_t relqlen = 0; + auto uri = link.uri.first; + auto len = link.uri.second - link.uri.first; - std::string authority, scheme; - http_parser_url v{}; - rv = http_parser_parse_url(link_url, link_urllen, 0, &v); + std::string scheme, authority, path; + + rv = http2::construct_push_component(scheme, authority, path, base, + baselen, uri, len); if (rv != 0) { - assert(link_urllen); - if (link_url[0] == '/') { - continue; - } - // treat link_url as relative URI. - auto end = std::find(link_url, link_url + link_urllen, '#'); - auto q = std::find(link_url, end, '?'); - rel = link_url; - rellen = q - link_url; - if (q != end) { - relq = q + 1; - relqlen = end - relq; - } - } else { - if (v.field_set & (1 << UF_SCHEMA)) { - http2::copy_url_component(scheme, &v, UF_SCHEMA, link_url); - } - - if (v.field_set & (1 << UF_HOST)) { - http2::copy_url_component(authority, &v, UF_HOST, link_url); - if (v.field_set & (1 << UF_PORT)) { - authority += ":"; - authority += util::utos(v.port); - } - } - - if (v.field_set & (1 << UF_PATH)) { - auto &f = v.field_data[UF_PATH]; - rel = link_url + f.off; - rellen = f.len; - } else { - rel = "/"; - rellen = 1; - } - - if (v.field_set & (1 << UF_QUERY)) { - auto &f = v.field_data[UF_QUERY]; - relq = link_url + f.off; - relqlen = f.len; - } + continue; } if (scheme.empty()) { @@ -1557,8 +1628,6 @@ int Http2Upstream::prepare_push_promise(Downstream *downstream) { authority = downstream->get_request_http2_authority(); } - auto path = http2::path_join(base, baselen, nullptr, 0, rel, rellen, relq, - relqlen); rv = submit_push_promise(scheme, authority, path, downstream); if (rv != 0) { return -1; @@ -1627,4 +1696,50 @@ int Http2Upstream::submit_push_promise(const std::string &scheme, return 0; } +int Http2Upstream::initiate_push(Downstream *downstream, const char *uri, + size_t len) { + int rv; + + if (len == 0 || get_config()->no_server_push || + nghttp2_session_get_remote_settings(session_, + NGHTTP2_SETTINGS_ENABLE_PUSH) == 0 || + get_config()->http2_proxy || get_config()->client_proxy || + (downstream->get_stream_id() % 2) == 0) { + return 0; + } + + const char *base; + size_t baselen; + + rv = http2::get_pure_path_component(&base, &baselen, + downstream->get_request_path()); + if (rv != 0) { + return -1; + } + + std::string scheme, authority, path; + + rv = http2::construct_push_component(scheme, authority, path, base, baselen, + uri, len); + if (rv != 0) { + return -1; + } + + if (scheme.empty()) { + scheme = downstream->get_request_http2_scheme(); + } + + if (authority.empty()) { + authority = downstream->get_request_http2_authority(); + } + + rv = submit_push_promise(scheme, authority, path, downstream); + + if (rv != 0) { + return -1; + } + + return 0; +} + } // namespace shrpx diff --git a/src/shrpx_http2_upstream.h b/src/shrpx_http2_upstream.h index 564f71f5..cc877f5c 100644 --- a/src/shrpx_http2_upstream.h +++ b/src/shrpx_http2_upstream.h @@ -78,6 +78,10 @@ public: virtual void on_handler_delete(); virtual int on_downstream_reset(bool no_retry); + virtual int send_reply(Downstream *downstream, const uint8_t *body, + size_t bodylen); + virtual int initiate_push(Downstream *downstream, const char *uri, + size_t len); bool get_flow_control() const; // Perform HTTP/2 upgrade from |upstream|. On success, this object diff --git a/src/shrpx_http_downstream_connection.cc b/src/shrpx_http_downstream_connection.cc index 43d1a300..8ce8904f 100644 --- a/src/shrpx_http_downstream_connection.cc +++ b/src/shrpx_http_downstream_connection.cc @@ -209,89 +209,54 @@ int HttpDownstreamConnection::attach_downstream(Downstream *downstream) { } int HttpDownstreamConnection::push_request_headers() { - const char *authority = nullptr, *host = nullptr; auto downstream_hostport = get_config() ->downstream_addr_groups[group_] .addrs[addr_idx_] .hostport.get(); - auto connect_method = downstream_->get_request_method() == HTTP_CONNECT; + auto method = downstream_->get_request_method(); + auto connect_method = method == HTTP_CONNECT; - if (!get_config()->no_host_rewrite && !get_config()->http2_proxy && - !get_config()->client_proxy && !connect_method) { - if (!downstream_->get_request_http2_authority().empty()) { - authority = downstream_hostport; - } - if (downstream_->get_request_header(http2::HD_HOST)) { - host = downstream_hostport; - } - } else { - if (!downstream_->get_request_http2_authority().empty()) { - authority = downstream_->get_request_http2_authority().c_str(); - } - auto h = downstream_->get_request_header(http2::HD_HOST); - if (h) { - host = h->value.c_str(); - } + // For HTTP/1.0 request, there is no authority in request. In that + // case, we use backend server's host nonetheless. + const char *authority = downstream_hostport; + auto &req_authority = downstream_->get_request_http2_authority(); + auto no_host_rewrite = get_config()->no_host_rewrite || + get_config()->http2_proxy || + get_config()->client_proxy || connect_method; + + if (no_host_rewrite && !req_authority.empty()) { + authority = req_authority.c_str(); } - if (!authority && !host) { - // upstream is HTTP/1.0. We use backend server's host - // nonetheless. - host = downstream_hostport; - } - - if (authority) { - downstream_->set_request_downstream_host(authority); - } else { - downstream_->set_request_downstream_host(host); - } + downstream_->set_request_downstream_host(authority); downstream_->assemble_request_cookie(); // Assume that method and request path do not contain \r\n. - std::string hdrs = http2::to_method_string(downstream_->get_request_method()); + std::string hdrs = http2::to_method_string(method); hdrs += ' '; auto &scheme = downstream_->get_request_http2_scheme(); + auto &path = downstream_->get_request_path(); if (connect_method) { - if (authority) { - hdrs += authority; - } else { - hdrs += downstream_->get_request_path(); - } + hdrs += authority; } else if (get_config()->http2_proxy || get_config()->client_proxy) { // Construct absolute-form request target because we are going to // send a request to a HTTP/1 proxy. assert(!scheme.empty()); hdrs += scheme; hdrs += "://"; - - if (authority) { - hdrs += authority; - } else { - hdrs += host; - } - - // Server-wide OPTIONS takes following form in proxy request: - // - // OPTIONS http://example.org HTTP/1.1 - // - // Notice that no slash after authority. See - // http://tools.ietf.org/html/rfc7230#section-5.3.4 - if (downstream_->get_request_path() != "*") { - hdrs += downstream_->get_request_path(); - } + hdrs += authority; + hdrs += path; + } else if (method == HTTP_OPTIONS && path.empty()) { + // Server-wide OPTIONS + hdrs += "*"; } else { - // No proxy case. - hdrs += downstream_->get_request_path(); + hdrs += path; } hdrs += " HTTP/1.1\r\nHost: "; - if (authority) { - hdrs += authority; - } else { - hdrs += host; - } + hdrs += authority; hdrs += "\r\n"; http2::build_http1_headers_from_headers(hdrs, @@ -774,6 +739,12 @@ int HttpDownstreamConnection::on_read() { auto htperr = HTTP_PARSER_ERRNO(&response_htp_); if (htperr != HPE_OK) { + // Handling early return (in other words, response was hijacked + // by mruby scripting). + if (downstream_->get_response_state() == Downstream::MSG_COMPLETE) { + return SHRPX_ERR_DCONN_CANCELED; + } + if (LOG_ENABLED(INFO)) { DCLOG(INFO, this) << "HTTP parser failure: " << "(" << http_errno_name(htperr) << ") " diff --git a/src/shrpx_https_upstream.cc b/src/shrpx_https_upstream.cc index 984a6d12..ca8a5778 100644 --- a/src/shrpx_https_upstream.cc +++ b/src/shrpx_https_upstream.cc @@ -37,6 +37,9 @@ #include "shrpx_log_config.h" #include "shrpx_worker.h" #include "shrpx_http2_session.h" +#ifdef HAVE_MRUBY +#include "shrpx_mruby.h" +#endif // HAVE_MRUBY #include "http2.h" #include "util.h" #include "template.h" @@ -69,8 +72,14 @@ int htp_msg_begin(http_parser *htp) { auto handler = upstream->get_client_handler(); // TODO specify 0 as priority for now - upstream->attach_downstream( - make_unique(upstream, handler->get_mcpool(), 0, 0)); + auto downstream = + make_unique(upstream, handler->get_mcpool(), 0, 0); + + // We happen to have the same value for method token. + downstream->set_request_method(htp->method); + + upstream->attach_downstream(std::move(downstream)); + return 0; } } // namespace @@ -90,7 +99,12 @@ int htp_uricb(http_parser *htp, const char *data, size_t len) { return -1; } downstream->add_request_headers_sum(len); - downstream->append_request_path(data, len); + if (downstream->get_request_method() == HTTP_CONNECT) { + downstream->append_request_http2_authority(data, len); + } else { + downstream->append_request_path(data, len); + } + return 0; } } // namespace @@ -212,7 +226,7 @@ void rewrite_request_host_path_from_uri(Downstream *downstream, const char *uri, // // Notice that no slash after authority. See // http://tools.ietf.org/html/rfc7230#section-5.3.4 - downstream->set_request_path("*"); + downstream->set_request_path(""); // we ignore query component here return; } else { @@ -241,17 +255,18 @@ int htp_hdrs_completecb(http_parser *htp) { } auto downstream = upstream->get_downstream(); - // We happen to have the same value for method token. - downstream->set_request_method(htp->method); downstream->set_request_major(htp->http_major); downstream->set_request_minor(htp->http_minor); downstream->set_request_connection_close(!http_should_keep_alive(htp)); + auto method = downstream->get_request_method(); + if (LOG_ENABLED(INFO)) { std::stringstream ss; - ss << http2::to_method_string(downstream->get_request_method()) << " " - << downstream->get_request_path() << " " + ss << http2::to_method_string(method) << " " + << (method == HTTP_CONNECT ? downstream->get_request_http2_authority() + : downstream->get_request_path()) << " " << "HTTP/" << downstream->get_request_major() << "." << downstream->get_request_minor() << "\n"; const auto &headers = downstream->get_request_headers(); @@ -274,13 +289,12 @@ int htp_hdrs_completecb(http_parser *htp) { downstream->inspect_http1_request(); - if (downstream->get_request_method() != HTTP_CONNECT) { + if (method != HTTP_CONNECT) { http_parser_url u{}; // make a copy of request path, since we may set request path // while we are refering to original request path. - auto uri = downstream->get_request_path(); - rv = http_parser_parse_url(uri.c_str(), - downstream->get_request_path().size(), 0, &u); + auto path = downstream->get_request_path(); + rv = http_parser_parse_url(path.c_str(), path.size(), 0, &u); if (rv != 0) { // Expect to respond with 400 bad request return -1; @@ -292,8 +306,17 @@ int htp_hdrs_completecb(http_parser *htp) { return -1; } - downstream->set_request_path( - http2::rewrite_clean_path(std::begin(uri), std::end(uri))); + if (method == HTTP_OPTIONS && path == "*") { + downstream->set_request_path(""); + } else { + downstream->set_request_path( + http2::rewrite_clean_path(std::begin(path), std::end(path))); + } + + auto host = downstream->get_request_header(http2::HD_HOST); + if (host) { + downstream->set_request_http2_authority(host->value); + } if (upstream->get_client_handler()->get_ssl()) { downstream->set_request_http2_scheme("https"); @@ -301,10 +324,29 @@ int htp_hdrs_completecb(http_parser *htp) { downstream->set_request_http2_scheme("http"); } } else { - rewrite_request_host_path_from_uri(downstream, uri.c_str(), u); + rewrite_request_host_path_from_uri(downstream, path.c_str(), u); } } + downstream->set_request_state(Downstream::HEADER_COMPLETE); + +#ifdef HAVE_MRUBY + auto handler = upstream->get_client_handler(); + auto worker = handler->get_worker(); + auto mruby_ctx = worker->get_mruby_context(); + + if (mruby_ctx->run_on_request_proc(downstream) != 0) { + downstream->set_response_http_status(500); + return -1; + } +#endif // HAVE_MRUBY + + // mruby hook may change method value + + if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { + return 0; + } + rv = downstream->attach_downstream_connection( upstream->get_client_handler()->get_downstream_connection(downstream)); @@ -320,8 +362,6 @@ int htp_hdrs_completecb(http_parser *htp) { return -1; } - downstream->set_request_state(Downstream::HEADER_COMPLETE); - return 0; } } // namespace @@ -352,6 +392,17 @@ int htp_msg_completecb(http_parser *htp) { downstream->set_request_state(Downstream::MSG_COMPLETE); rv = downstream->end_upload_data(); if (rv != 0) { + if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { + // Here both response and request were completed. One of the + // reason why end_upload_data() failed is when we sent response + // in request phase hook. We only delete and proceed to the + // next request handling (if we don't close the connection). We + // first pause parser here jsut as we normally do, and call + // signal_write() to run on_write(). + http_parser_pause(htp, 1); + + return 0; + } return -1; } @@ -451,6 +502,13 @@ int HttpsUpstream::on_read() { auto htperr = HTTP_PARSER_ERRNO(&htp_); if (htperr == HPE_PAUSED) { + // We may pause parser in htp_msg_completecb when both side are + // completed. Signal write, so that we can run on_write(). + if (downstream && + downstream->get_request_state() == Downstream::MSG_COMPLETE && + downstream->get_response_state() == Downstream::MSG_COMPLETE) { + handler_->signal_write(); + } return 0; } @@ -472,13 +530,16 @@ int HttpsUpstream::on_read() { if (htperr == HPE_INVALID_METHOD) { status_code = 501; } else if (downstream) { - if (downstream->get_request_state() == Downstream::CONNECT_FAIL) { - status_code = 503; - } else if (downstream->get_request_state() == - Downstream::HTTP1_REQUEST_HEADER_TOO_LARGE) { - status_code = 431; - } else { - status_code = 400; + status_code = downstream->get_response_http_status(); + if (status_code == 0) { + if (downstream->get_request_state() == Downstream::CONNECT_FAIL) { + status_code = 503; + } else if (downstream->get_request_state() == + Downstream::HTTP1_REQUEST_HEADER_TOO_LARGE) { + status_code = 431; + } else { + status_code = 400; + } } } else { status_code = 400; @@ -552,6 +613,11 @@ int HttpsUpstream::on_write() { // We need this if response ends before request. if (downstream->get_request_state() == Downstream::MSG_COMPLETE) { delete_downstream(); + + if (handler_->get_should_close_after_write()) { + return 0; + } + return resume_read(SHRPX_NO_BUFFER, nullptr, 0); } } @@ -594,6 +660,11 @@ int HttpsUpstream::downstream_read(DownstreamConnection *dconn) { return downstream_eof(dconn); } + if (rv == SHRPX_ERR_DCONN_CANCELED) { + downstream->pop_downstream_connection(); + goto end; + } + if (rv < 0) { return downstream_error(dconn, Downstream::EVENT_ERROR); } @@ -703,6 +774,63 @@ int HttpsUpstream::downstream_error(DownstreamConnection *dconn, int events) { return 0; } +int HttpsUpstream::send_reply(Downstream *downstream, const uint8_t *body, + size_t bodylen) { + auto major = downstream->get_request_major(); + auto minor = downstream->get_request_minor(); + + auto connection_close = false; + if (major <= 0 || (major == 1 && minor == 0)) { + connection_close = true; + } else { + auto c = downstream->get_response_header(http2::HD_CONNECTION); + if (c && util::strieq_l("close", c->value)) { + connection_close = true; + } + } + + if (connection_close) { + downstream->set_response_connection_close(true); + handler_->set_should_close_after_write(true); + } + + auto output = downstream->get_response_buf(); + + output->append("HTTP/1.1 "); + auto status_str = + http2::get_status_string(downstream->get_response_http_status()); + output->append(status_str.c_str(), status_str.size()); + output->append("\r\n"); + + for (auto &kv : downstream->get_response_headers()) { + if (kv.name.empty() || kv.name[0] == ':') { + continue; + } + auto name = kv.name; + http2::capitalize(name, 0); + output->append(name.c_str(), name.size()); + output->append(": "); + output->append(kv.value.c_str(), kv.value.size()); + output->append("\r\n"); + } + + if (!downstream->get_response_header(http2::HD_SERVER)) { + output->append("Server: "); + output->append(get_config()->server_name, + strlen(get_config()->server_name)); + output->append("\r\n"); + } + + output->append("\r\n"); + + output->append(body, bodylen); + + downstream->add_response_sent_bodylen(bodylen); + downstream->set_response_state(Downstream::MSG_COMPLETE); + + return 0; +} + void HttpsUpstream::error_reply(unsigned int status_code) { auto html = http::create_error_html(status_code); auto downstream = get_downstream(); @@ -765,6 +893,22 @@ int HttpsUpstream::on_downstream_header_complete(Downstream *downstream) { } } +#ifdef HAVE_MRUBY + if (!downstream->get_non_final_response()) { + auto worker = handler_->get_worker(); + auto mruby_ctx = worker->get_mruby_context(); + + if (mruby_ctx->run_on_response_proc(downstream) != 0) { + error_reply(500); + return -1; + } + + if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { + return -1; + } + } +#endif // HAVE_MRUBY + auto connect_method = downstream->get_request_method() == HTTP_CONNECT; std::string hdrs = "HTTP/"; @@ -1016,4 +1160,9 @@ fail: return 0; } +int HttpsUpstream::initiate_push(Downstream *downstream, const char *uri, + size_t len) { + return 0; +} + } // namespace shrpx diff --git a/src/shrpx_https_upstream.h b/src/shrpx_https_upstream.h index 3b9d5320..318200ed 100644 --- a/src/shrpx_https_upstream.h +++ b/src/shrpx_https_upstream.h @@ -74,6 +74,10 @@ public: virtual void on_handler_delete(); virtual int on_downstream_reset(bool no_retry); + virtual int send_reply(Downstream *downstream, const uint8_t *body, + size_t bodylen); + virtual int initiate_push(Downstream *downstream, const char *uri, + size_t len); void reset_current_header_length(); void log_response_headers(const std::string &hdrs) const; diff --git a/src/shrpx_mruby.cc b/src/shrpx_mruby.cc new file mode 100644 index 00000000..aadca674 --- /dev/null +++ b/src/shrpx_mruby.cc @@ -0,0 +1,203 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * 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 "shrpx_mruby.h" + +#include +#include + +#include "shrpx_downstream.h" +#include "shrpx_config.h" +#include "shrpx_mruby_module.h" +#include "shrpx_downstream_connection.h" +#include "template.h" + +namespace shrpx { + +namespace mruby { + +MRubyContext::MRubyContext(mrb_state *mrb, RProc *on_request_proc, + RProc *on_response_proc) + : mrb_(mrb), on_request_proc_(on_request_proc), + on_response_proc_(on_response_proc), running_(false) {} + +MRubyContext::~MRubyContext() { mrb_close(mrb_); } + +int MRubyContext::run_request_proc(Downstream *downstream, RProc *proc, + int phase) { + if (!proc || running_) { + return 0; + } + + running_ = true; + + MRubyAssocData data{downstream, phase}; + + mrb_->ud = &data; + + int rv = 0; + auto ai = mrb_gc_arena_save(mrb_); + + auto res = mrb_run(mrb_, proc, mrb_top_self(mrb_)); + (void)res; + + if (mrb_->exc) { + // If response has been committed, ignore error + if (downstream->get_response_state() != Downstream::MSG_COMPLETE) { + rv = -1; + } + auto error = + mrb_str_ptr(mrb_funcall(mrb_, mrb_obj_value(mrb_->exc), "inspect", 0)); + + LOG(ERROR) << "Exception caught while executing mruby code: " + << error->as.heap.ptr; + mrb_->exc = 0; + } + + mrb_->ud = nullptr; + + mrb_gc_arena_restore(mrb_, ai); + + if (data.request_headers_dirty) { + downstream->index_request_headers(); + } + + if (data.response_headers_dirty) { + downstream->index_response_headers(); + } + + running_ = false; + + return rv; +} + +int MRubyContext::run_on_request_proc(Downstream *downstream) { + return run_request_proc(downstream, on_request_proc_, PHASE_REQUEST); +} + +int MRubyContext::run_on_response_proc(Downstream *downstream) { + return run_request_proc(downstream, on_response_proc_, PHASE_RESPONSE); +} + +void MRubyContext::delete_downstream(Downstream *downstream) { + if (!mrb_) { + return; + } + delete_downstream_from_module(mrb_, downstream); +} + +// Based on +// https://github.com/h2o/h2o/blob/master/lib/handler/mruby.c. It is +// very hard to write these kind of code because mruby has almost no +// documentation aobut compiling or generating code, at least at the +// time of this writing. +RProc *compile(mrb_state *mrb, const char *filename) { + if (filename == nullptr) { + return nullptr; + } + + auto infile = fopen(filename, "rb"); + if (infile == nullptr) { + return nullptr; + } + auto infile_d = defer(fclose, infile); + + auto mrbc = mrbc_context_new(mrb); + if (mrbc == nullptr) { + LOG(ERROR) << "mrb_context_new failed"; + return nullptr; + } + auto mrbc_d = defer(mrbc_context_free, mrb, mrbc); + + auto parser = mrb_parse_file(mrb, infile, nullptr); + if (parser == nullptr) { + LOG(ERROR) << "mrb_parse_nstring failed"; + return nullptr; + } + auto parser_d = defer(mrb_parser_free, parser); + + if (parser->nerr != 0) { + LOG(ERROR) << "mruby parser detected parse error"; + return nullptr; + } + + auto proc = mrb_generate_code(mrb, parser); + if (proc == nullptr) { + LOG(ERROR) << "mrb_generate_code failed"; + return nullptr; + } + + return proc; +} + +std::unique_ptr create_mruby_context() { + auto req_file = get_config()->request_phase_file.get(); + auto res_file = get_config()->response_phase_file.get(); + + if (!req_file && !res_file) { + return make_unique(nullptr, nullptr, nullptr); + } + + auto mrb = mrb_open(); + if (mrb == nullptr) { + LOG(ERROR) << "mrb_open failed"; + return nullptr; + } + + init_module(mrb); + + auto req_proc = compile(mrb, req_file); + + if (req_file && !req_proc) { + LOG(ERROR) << "Could not compile mruby code " << req_file; + mrb_close(mrb); + return nullptr; + } + + auto res_proc = compile(mrb, res_file); + + if (res_file && !res_proc) { + LOG(ERROR) << "Could not compile mruby code " << res_file; + mrb_close(mrb); + return nullptr; + } + + return make_unique(mrb, req_proc, res_proc); +} + +mrb_sym intern_ptr(mrb_state *mrb, void *ptr) { + auto p = reinterpret_cast(ptr); + + return mrb_intern(mrb, reinterpret_cast(&p), sizeof(p)); +} + +void check_phase(mrb_state *mrb, int phase, int phase_mask) { + if ((phase & phase_mask) == 0) { + mrb_raise(mrb, E_RUNTIME_ERROR, "operation was not allowed in this phase"); + } +} + +} // namespace mruby + +} // namespace shrpx diff --git a/src/shrpx_mruby.h b/src/shrpx_mruby.h new file mode 100644 index 00000000..a75e4975 --- /dev/null +++ b/src/shrpx_mruby.h @@ -0,0 +1,88 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * 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. + */ +#ifndef SHRPX_MRUBY_H +#define SHRPX_MRUBY_H + +#include "shrpx.h" + +#include + +#include +#include + +using namespace nghttp2; + +namespace shrpx { + +namespace mruby { + +class MRubyContext { +public: + MRubyContext(mrb_state *mrb, RProc *on_request_proc, RProc *on_response_proc); + ~MRubyContext(); + + int run_on_request_proc(Downstream *downstream); + int run_on_response_proc(Downstream *downstream); + + int run_request_proc(Downstream *downstream, RProc *proc, int phase); + + void delete_downstream(Downstream *downstream); + +private: + mrb_state *mrb_; + RProc *on_request_proc_; + RProc *on_response_proc_; + bool running_; +}; + +enum { + PHASE_NONE = 0, + PHASE_REQUEST = 1, + PHASE_RESPONSE = 1 << 1, +}; + +struct MRubyAssocData { + Downstream *downstream; + int phase; + bool request_headers_dirty; + bool response_headers_dirty; +}; + +RProc *compile(mrb_state *mrb, const char *filename); + +std::unique_ptr create_mruby_context(); + +// Return interned |ptr|. +mrb_sym intern_ptr(mrb_state *mrb, void *ptr); + +// Checks that |phase| is set in |phase_mask|. If not set, raise +// exception. +void check_phase(mrb_state *mrb, int phase, int phase_mask); + +} // namespace mruby + +} // namespace shrpx + +#endif // SHRPX_MRUBY_H diff --git a/src/shrpx_mruby_module.cc b/src/shrpx_mruby_module.cc new file mode 100644 index 00000000..e5e397ee --- /dev/null +++ b/src/shrpx_mruby_module.cc @@ -0,0 +1,128 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * 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 "shrpx_mruby_module.h" + +#include + +#include +#include +#include +#include + +#include "shrpx_mruby.h" +#include "shrpx_mruby_module_env.h" +#include "shrpx_mruby_module_request.h" +#include "shrpx_mruby_module_response.h" + +namespace shrpx { + +namespace mruby { + +namespace { +mrb_value run(mrb_state *mrb, mrb_value self) { + mrb_value b; + mrb_get_args(mrb, "&", &b); + + if (mrb_nil_p(b)) { + return mrb_nil_value(); + } + + auto module = mrb_module_get(mrb, "Nghttpx"); + + auto env_sym = mrb_intern_lit(mrb, "env"); + auto env = mrb_obj_iv_get(mrb, reinterpret_cast(module), env_sym); + + if (mrb_nil_p(env)) { + auto env_class = mrb_class_get_under(mrb, module, "Env"); + auto request_class = mrb_class_get_under(mrb, module, "Request"); + auto response_class = mrb_class_get_under(mrb, module, "Response"); + + env = mrb_obj_new(mrb, env_class, 0, nullptr); + auto req = mrb_obj_new(mrb, request_class, 0, nullptr); + auto resp = mrb_obj_new(mrb, response_class, 0, nullptr); + + mrb_iv_set(mrb, env, mrb_intern_lit(mrb, "req"), req); + mrb_iv_set(mrb, env, mrb_intern_lit(mrb, "resp"), resp); + + mrb_obj_iv_set(mrb, reinterpret_cast(module), env_sym, env); + } + + std::array args{{env}}; + return mrb_yield_argv(mrb, b, args.size(), args.data()); +} +} // namespace + +void delete_downstream_from_module(mrb_state *mrb, Downstream *downstream) { + auto module = mrb_module_get(mrb, "Nghttpx"); + auto env = mrb_obj_iv_get(mrb, reinterpret_cast(module), + mrb_intern_lit(mrb, "env")); + if (mrb_nil_p(env)) { + return; + } + + mrb_iv_remove(mrb, env, intern_ptr(mrb, downstream)); +} + +void init_module(mrb_state *mrb) { + auto module = mrb_define_module(mrb, "Nghttpx"); + + mrb_define_class_method(mrb, module, "run", run, + MRB_ARGS_REQ(1) | MRB_ARGS_BLOCK()); + mrb_define_const(mrb, module, "REQUEST_PHASE", + mrb_fixnum_value(PHASE_REQUEST)); + mrb_define_const(mrb, module, "RESPONSE_PHASE", + mrb_fixnum_value(PHASE_RESPONSE)); + + init_env_class(mrb, module); + init_request_class(mrb, module); + init_response_class(mrb, module); +} + +mrb_value create_headers_hash(mrb_state *mrb, const Headers &headers) { + auto hash = mrb_hash_new(mrb); + + for (auto &hd : headers) { + if (hd.name.empty() || hd.name[0] == ':') { + continue; + } + auto ai = mrb_gc_arena_save(mrb); + + auto key = mrb_str_new(mrb, hd.name.c_str(), hd.name.size()); + auto ary = mrb_hash_get(mrb, hash, key); + if (mrb_nil_p(ary)) { + ary = mrb_ary_new(mrb); + mrb_hash_set(mrb, hash, key, ary); + } + mrb_ary_push(mrb, ary, mrb_str_new(mrb, hd.value.c_str(), hd.value.size())); + + mrb_gc_arena_restore(mrb, ai); + } + + return hash; +} + +} // namespace mruby + +} // namespace shrpx diff --git a/src/shrpx_mruby_module.h b/src/shrpx_mruby_module.h new file mode 100644 index 00000000..8d5274e2 --- /dev/null +++ b/src/shrpx_mruby_module.h @@ -0,0 +1,52 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * 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. + */ +#ifndef SHRPX_MRUBY_MODULE_H +#define SHRPX_MRUBY_MODULE_H + +#include "shrpx.h" + +#include + +#include "http2.h" + +using namespace nghttp2; + +namespace shrpx { + +class Downstream; + +namespace mruby { + +void init_module(mrb_state *mrb); + +void delete_downstream_from_module(mrb_state *mrb, Downstream *downstream); + +mrb_value create_headers_hash(mrb_state *mrb, const Headers &headers); + +} // namespace mruby + +} // namespace shrpx + +#endif // SHRPX_MRUBY_MODULE_H diff --git a/src/shrpx_mruby_module_env.cc b/src/shrpx_mruby_module_env.cc new file mode 100644 index 00000000..1fb75781 --- /dev/null +++ b/src/shrpx_mruby_module_env.cc @@ -0,0 +1,110 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * 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 "shrpx_mruby_module_env.h" + +#include +#include +#include + +#include "shrpx_downstream.h" +#include "shrpx_upstream.h" +#include "shrpx_client_handler.h" +#include "shrpx_mruby.h" +#include "shrpx_mruby_module.h" + +namespace shrpx { + +namespace mruby { + +namespace { +mrb_value env_init(mrb_state *mrb, mrb_value self) { return self; } +} // namespace + +namespace { +mrb_value env_get_req(mrb_state *mrb, mrb_value self) { + return mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "req")); +} +} // namespace + +namespace { +mrb_value env_get_resp(mrb_state *mrb, mrb_value self) { + return mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "resp")); +} +} // namespace + +namespace { +mrb_value env_get_ctx(mrb_state *mrb, mrb_value self) { + auto data = reinterpret_cast(mrb->ud); + auto downstream = data->downstream; + + auto dsym = intern_ptr(mrb, downstream); + + auto ctx = mrb_iv_get(mrb, self, dsym); + if (mrb_nil_p(ctx)) { + ctx = mrb_hash_new(mrb); + mrb_iv_set(mrb, self, dsym, ctx); + } + + return ctx; +} +} // namespace + +namespace { +mrb_value env_get_phase(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + + return mrb_fixnum_value(data->phase); +} +} // namespace + +namespace { +mrb_value env_get_remote_addr(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + + auto &ipaddr = handler->get_ipaddr(); + + return mrb_str_new(mrb, ipaddr.c_str(), ipaddr.size()); +} +} // namespace + +void init_env_class(mrb_state *mrb, RClass *module) { + auto env_class = + mrb_define_class_under(mrb, module, "Env", mrb->object_class); + + mrb_define_method(mrb, env_class, "initialize", env_init, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "req", env_get_req, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "resp", env_get_resp, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "ctx", env_get_ctx, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "phase", env_get_phase, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "remote_addr", env_get_remote_addr, + MRB_ARGS_NONE()); +} + +} // namespace mruby + +} // namespace shrpx diff --git a/src/shrpx_mruby_module_env.h b/src/shrpx_mruby_module_env.h new file mode 100644 index 00000000..7515e16e --- /dev/null +++ b/src/shrpx_mruby_module_env.h @@ -0,0 +1,44 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * 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. + */ +#ifndef SHRPX_MRUBY_MODULE_ENV_H +#define SHRPX_MRUBY_MODULE_ENV_H + +#include "shrpx.h" + +#include + +using namespace nghttp2; + +namespace shrpx { + +namespace mruby { + +void init_env_class(mrb_state *mrb, RClass *module); + +} // namespace mruby + +} // namespace shrpx + +#endif // SHRPX_MRUBY_MODULE_ENV_H diff --git a/src/shrpx_mruby_module_request.cc b/src/shrpx_mruby_module_request.cc new file mode 100644 index 00000000..10ef99ec --- /dev/null +++ b/src/shrpx_mruby_module_request.cc @@ -0,0 +1,326 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * 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 "shrpx_mruby_module_request.h" + +#include +#include +#include +#include + +#include "shrpx_downstream.h" +#include "shrpx_upstream.h" +#include "shrpx_client_handler.h" +#include "shrpx_mruby.h" +#include "shrpx_mruby_module.h" +#include "util.h" +#include "http2.h" + +namespace shrpx { + +namespace mruby { + +namespace { +mrb_value request_init(mrb_state *mrb, mrb_value self) { return self; } +} // namespace + +namespace { +mrb_value request_get_http_version_major(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + return mrb_fixnum_value(downstream->get_request_major()); +} +} // namespace + +namespace { +mrb_value request_get_http_version_minor(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + return mrb_fixnum_value(downstream->get_request_minor()); +} +} // namespace + +namespace { +mrb_value request_get_method(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + auto method = http2::to_method_string(downstream->get_request_method()); + + return mrb_str_new_cstr(mrb, method); +} +} // namespace + +namespace { +mrb_value request_set_method(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + + check_phase(mrb, data->phase, PHASE_REQUEST); + + const char *method; + mrb_int n; + mrb_get_args(mrb, "s", &method, &n); + if (n == 0) { + mrb_raise(mrb, E_RUNTIME_ERROR, "method must not be empty string"); + } + auto token = + http2::lookup_method_token(reinterpret_cast(method), n); + if (token == -1) { + mrb_raise(mrb, E_RUNTIME_ERROR, "method not supported"); + } + + downstream->set_request_method(token); + + return self; +} +} // namespace + +namespace { +mrb_value request_get_authority(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + auto &authority = downstream->get_request_http2_authority(); + + return mrb_str_new(mrb, authority.c_str(), authority.size()); +} +} // namespace + +namespace { +mrb_value request_set_authority(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + + check_phase(mrb, data->phase, PHASE_REQUEST); + + const char *authority; + mrb_int n; + mrb_get_args(mrb, "s", &authority, &n); + if (n == 0) { + mrb_raise(mrb, E_RUNTIME_ERROR, "authority must not be empty string"); + } + + downstream->set_request_http2_authority(std::string(authority, n)); + + return self; +} +} // namespace + +namespace { +mrb_value request_get_scheme(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + auto &scheme = downstream->get_request_http2_scheme(); + + return mrb_str_new(mrb, scheme.c_str(), scheme.size()); +} +} // namespace + +namespace { +mrb_value request_set_scheme(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + + check_phase(mrb, data->phase, PHASE_REQUEST); + + const char *scheme; + mrb_int n; + mrb_get_args(mrb, "s", &scheme, &n); + if (n == 0) { + mrb_raise(mrb, E_RUNTIME_ERROR, "scheme must not be empty string"); + } + + downstream->set_request_http2_scheme(std::string(scheme, n)); + + return self; +} +} // namespace + +namespace { +mrb_value request_get_path(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + auto &path = downstream->get_request_path(); + + return mrb_str_new(mrb, path.c_str(), path.size()); +} +} // namespace + +namespace { +mrb_value request_set_path(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + + check_phase(mrb, data->phase, PHASE_REQUEST); + + const char *path; + mrb_int pathlen; + mrb_get_args(mrb, "s", &path, &pathlen); + + downstream->set_request_path(std::string(path, pathlen)); + + return self; +} +} // namespace + +namespace { +mrb_value request_get_headers(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + return create_headers_hash(mrb, downstream->get_request_headers()); +} +} // namespace + +namespace { +mrb_value request_mod_header(mrb_state *mrb, mrb_value self, bool repl) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + + check_phase(mrb, data->phase, PHASE_REQUEST); + + mrb_value key, values; + mrb_get_args(mrb, "oo", &key, &values); + + if (RSTRING_LEN(key) == 0) { + mrb_raise(mrb, E_RUNTIME_ERROR, "empty key is not allowed"); + } + + key = mrb_funcall(mrb, key, "downcase", 0); + + if (repl) { + size_t p = 0; + auto &headers = downstream->get_request_headers(); + for (size_t i = 0; i < headers.size(); ++i) { + auto &hd = headers[i]; + if (util::streq(std::begin(hd.name), hd.name.size(), RSTRING_PTR(key), + RSTRING_LEN(key))) { + continue; + } + if (i != p) { + headers[p++] = std::move(hd); + } + } + headers.resize(p); + } + + if (mrb_obj_is_instance_of(mrb, values, mrb->array_class)) { + auto n = mrb_ary_len(mrb, values); + for (int i = 0; i < n; ++i) { + auto value = mrb_ary_entry(values, i); + downstream->add_request_header( + std::string(RSTRING_PTR(key), RSTRING_LEN(key)), + std::string(RSTRING_PTR(value), RSTRING_LEN(value))); + } + } else if (!mrb_nil_p(values)) { + downstream->add_request_header( + std::string(RSTRING_PTR(key), RSTRING_LEN(key)), + std::string(RSTRING_PTR(values), RSTRING_LEN(values))); + } + + data->request_headers_dirty = true; + + return mrb_nil_value(); +} +} // namespace + +namespace { +mrb_value request_set_header(mrb_state *mrb, mrb_value self) { + return request_mod_header(mrb, self, true); +} +} // namespace + +namespace { +mrb_value request_add_header(mrb_state *mrb, mrb_value self) { + return request_mod_header(mrb, self, false); +} +} // namespace + +namespace { +mrb_value request_clear_headers(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + + check_phase(mrb, data->phase, PHASE_REQUEST); + + downstream->clear_request_headers(); + + return mrb_nil_value(); +} +} // namespace + +namespace { +mrb_value request_push(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + + const char *uri; + mrb_int len; + mrb_get_args(mrb, "s", &uri, &len); + + upstream->initiate_push(downstream, uri, len); + + return mrb_nil_value(); +} +} // namespace + +void init_request_class(mrb_state *mrb, RClass *module) { + auto request_class = + mrb_define_class_under(mrb, module, "Request", mrb->object_class); + + mrb_define_method(mrb, request_class, "initialize", request_init, + MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "http_version_major", + request_get_http_version_major, MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "http_version_minor", + request_get_http_version_minor, MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "method", request_get_method, + MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "method=", request_set_method, + MRB_ARGS_REQ(1)); + mrb_define_method(mrb, request_class, "authority", request_get_authority, + MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "authority=", request_set_authority, + MRB_ARGS_REQ(1)); + mrb_define_method(mrb, request_class, "scheme", request_get_scheme, + MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "scheme=", request_set_scheme, + MRB_ARGS_REQ(1)); + mrb_define_method(mrb, request_class, "path", request_get_path, + MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "path=", request_set_path, + MRB_ARGS_REQ(1)); + mrb_define_method(mrb, request_class, "headers", request_get_headers, + MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "add_header", request_add_header, + MRB_ARGS_REQ(2)); + mrb_define_method(mrb, request_class, "set_header", request_set_header, + MRB_ARGS_REQ(2)); + mrb_define_method(mrb, request_class, "clear_headers", request_clear_headers, + MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "push", request_push, MRB_ARGS_REQ(1)); +} + +} // namespace mruby + +} // namespace shrpx diff --git a/src/shrpx_mruby_module_request.h b/src/shrpx_mruby_module_request.h new file mode 100644 index 00000000..b27c569b --- /dev/null +++ b/src/shrpx_mruby_module_request.h @@ -0,0 +1,44 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * 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. + */ +#ifndef SHRPX_MRUBY_MODULE_REQUEST_H +#define SHRPX_MRUBY_MODULE_REQUEST_H + +#include "shrpx.h" + +#include + +using namespace nghttp2; + +namespace shrpx { + +namespace mruby { + +void init_request_class(mrb_state *mrb, RClass *module); + +} // namespace mruby + +} // namespace shrpx + +#endif // SHRPX_MRUBY_MODULE_REQUEST_H diff --git a/src/shrpx_mruby_module_response.cc b/src/shrpx_mruby_module_response.cc new file mode 100644 index 00000000..0b49e925 --- /dev/null +++ b/src/shrpx_mruby_module_response.cc @@ -0,0 +1,255 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * 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 "shrpx_mruby_module_response.h" + +#include +#include +#include +#include + +#include "shrpx_downstream.h" +#include "shrpx_upstream.h" +#include "shrpx_client_handler.h" +#include "shrpx_mruby.h" +#include "shrpx_mruby_module.h" +#include "util.h" +#include "http2.h" + +namespace shrpx { + +namespace mruby { + +namespace { +mrb_value response_init(mrb_state *mrb, mrb_value self) { return self; } +} // namespace + +namespace { +mrb_value response_get_http_version_major(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + return mrb_fixnum_value(downstream->get_response_major()); +} +} // namespace + +namespace { +mrb_value response_get_http_version_minor(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + return mrb_fixnum_value(downstream->get_response_minor()); +} +} // namespace + +namespace { +mrb_value response_get_status(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + + return mrb_fixnum_value(downstream->get_response_http_status()); +} +} // namespace + +namespace { +mrb_value response_set_status(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + + mrb_int status; + mrb_get_args(mrb, "i", &status); + // We don't support 1xx status code for mruby scripting yet. + if (status < 200 || status > 999) { + mrb_raise(mrb, E_RUNTIME_ERROR, + "invalid status; it should be [200, 999], inclusive"); + } + + downstream->set_response_http_status(status); + + return self; +} +} // namespace + +namespace { +mrb_value response_get_headers(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + return create_headers_hash(mrb, downstream->get_response_headers()); +} +} // namespace + +namespace { +mrb_value response_mod_header(mrb_state *mrb, mrb_value self, bool repl) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + + mrb_value key, values; + mrb_get_args(mrb, "oo", &key, &values); + + if (RSTRING_LEN(key) == 0) { + mrb_raise(mrb, E_RUNTIME_ERROR, "empty key is not allowed"); + } + + key = mrb_funcall(mrb, key, "downcase", 0); + + if (repl) { + size_t p = 0; + auto &headers = downstream->get_response_headers(); + for (size_t i = 0; i < headers.size(); ++i) { + auto &hd = headers[i]; + if (util::streq(std::begin(hd.name), hd.name.size(), RSTRING_PTR(key), + RSTRING_LEN(key))) { + continue; + } + if (i != p) { + headers[p++] = std::move(hd); + } + } + headers.resize(p); + } + + if (mrb_obj_is_instance_of(mrb, values, mrb->array_class)) { + auto n = mrb_ary_len(mrb, values); + for (int i = 0; i < n; ++i) { + auto value = mrb_ary_entry(values, i); + downstream->add_response_header( + std::string(RSTRING_PTR(key), RSTRING_LEN(key)), + std::string(RSTRING_PTR(value), RSTRING_LEN(value))); + } + } else if (!mrb_nil_p(values)) { + downstream->add_response_header( + std::string(RSTRING_PTR(key), RSTRING_LEN(key)), + std::string(RSTRING_PTR(values), RSTRING_LEN(values))); + } + + data->response_headers_dirty = true; + + return mrb_nil_value(); +} +} // namespace + +namespace { +mrb_value response_set_header(mrb_state *mrb, mrb_value self) { + return response_mod_header(mrb, self, true); +} +} // namespace + +namespace { +mrb_value response_add_header(mrb_state *mrb, mrb_value self) { + return response_mod_header(mrb, self, false); +} +} // namespace + +namespace { +mrb_value response_clear_headers(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + + downstream->clear_response_headers(); + + return mrb_nil_value(); +} +} // namespace + +namespace { +mrb_value response_return(mrb_state *mrb, mrb_value self) { + auto data = static_cast(mrb->ud); + auto downstream = data->downstream; + int rv; + + if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { + mrb_raise(mrb, E_RUNTIME_ERROR, "response has already been committed"); + } + + mrb_value val; + mrb_get_args(mrb, "|o", &val); + + const uint8_t *body = nullptr; + size_t bodylen = 0; + + if (downstream->get_response_http_status() == 0) { + downstream->set_response_http_status(200); + } + + if (data->response_headers_dirty) { + downstream->index_response_headers(); + data->response_headers_dirty = false; + } + + if (downstream->expect_response_body() && !mrb_nil_p(val)) { + body = reinterpret_cast(RSTRING_PTR(val)); + bodylen = RSTRING_LEN(val); + } + + auto cl = downstream->get_response_header(http2::HD_CONTENT_LENGTH); + if (cl) { + cl->value = util::utos(bodylen); + } else { + downstream->add_response_header("content-length", util::utos(bodylen), + http2::HD_CONTENT_LENGTH); + } + downstream->set_response_content_length(bodylen); + + auto upstream = downstream->get_upstream(); + + rv = upstream->send_reply(downstream, body, bodylen); + if (rv != 0) { + mrb_raise(mrb, E_RUNTIME_ERROR, "could not send response"); + } + + auto handler = upstream->get_client_handler(); + + handler->signal_write(); + + return self; +} +} // namespace + +void init_response_class(mrb_state *mrb, RClass *module) { + auto response_class = + mrb_define_class_under(mrb, module, "Response", mrb->object_class); + + mrb_define_method(mrb, response_class, "initialize", response_init, + MRB_ARGS_NONE()); + mrb_define_method(mrb, response_class, "http_version_major", + response_get_http_version_major, MRB_ARGS_NONE()); + mrb_define_method(mrb, response_class, "http_version_minor", + response_get_http_version_minor, MRB_ARGS_NONE()); + mrb_define_method(mrb, response_class, "status", response_get_status, + MRB_ARGS_NONE()); + mrb_define_method(mrb, response_class, "status=", response_set_status, + MRB_ARGS_REQ(1)); + mrb_define_method(mrb, response_class, "headers", response_get_headers, + MRB_ARGS_NONE()); + mrb_define_method(mrb, response_class, "add_header", response_add_header, + MRB_ARGS_REQ(2)); + mrb_define_method(mrb, response_class, "set_header", response_set_header, + MRB_ARGS_REQ(2)); + mrb_define_method(mrb, response_class, "clear_headers", + response_clear_headers, MRB_ARGS_NONE()); + mrb_define_method(mrb, response_class, "return", response_return, + MRB_ARGS_OPT(1)); +} + +} // namespace mruby + +} // namespace shrpx diff --git a/src/shrpx_mruby_module_response.h b/src/shrpx_mruby_module_response.h new file mode 100644 index 00000000..32ed0d41 --- /dev/null +++ b/src/shrpx_mruby_module_response.h @@ -0,0 +1,44 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * 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. + */ +#ifndef SHRPX_MRUBY_MODULE_RESPONSE_H +#define SHRPX_MRUBY_MODULE_RESPONSE_H + +#include "shrpx.h" + +#include + +using namespace nghttp2; + +namespace shrpx { + +namespace mruby { + +void init_response_class(mrb_state *mrb, RClass *module); + +} // namespace mruby + +} // namespace shrpx + +#endif // SHRPX_MRUBY_MODULE_RESPONSE_H diff --git a/src/shrpx_spdy_upstream.cc b/src/shrpx_spdy_upstream.cc index 89fcb678..d3dea3f9 100644 --- a/src/shrpx_spdy_upstream.cc +++ b/src/shrpx_spdy_upstream.cc @@ -36,6 +36,11 @@ #include "shrpx_downstream_connection.h" #include "shrpx_config.h" #include "shrpx_http.h" +#ifdef HAVE_MRUBY +#include "shrpx_mruby.h" +#endif // HAVE_MRUBY +#include "shrpx_worker.h" +#include "shrpx_http2_session.h" #include "http2.h" #include "util.h" #include "template.h" @@ -224,6 +229,8 @@ void on_ctrl_recv_callback(spdylay_session *session, spdylay_frame_type type, downstream->set_request_http2_authority(host->value); if (get_config()->http2_proxy || get_config()->client_proxy) { downstream->set_request_path(path->value); + } else if (method_token == HTTP_OPTIONS && path->value == "*") { + // Server-wide OPTIONS request. Path is empty. } else { downstream->set_request_path(http2::rewrite_clean_path( std::begin(path->value), std::end(path->value))); @@ -237,6 +244,21 @@ void on_ctrl_recv_callback(spdylay_session *session, spdylay_frame_type type, downstream->inspect_http2_request(); downstream->set_request_state(Downstream::HEADER_COMPLETE); + +#ifdef HAVE_MRUBY + auto handler = upstream->get_client_handler(); + auto worker = handler->get_worker(); + auto mruby_ctx = worker->get_mruby_context(); + + if (mruby_ctx->run_on_request_proc(downstream) != 0) { + if (upstream->error_reply(downstream, 500) != 0) { + ULOG(FATAL, upstream) << "error_reply failed"; + return; + } + return; + } +#endif // HAVE_MRUBY + if (frame->syn_stream.hd.flags & SPDYLAY_CTRL_FLAG_FIN) { if (!downstream->validate_request_bodylen()) { upstream->rst_stream(downstream, SPDYLAY_PROTOCOL_ERROR); @@ -247,6 +269,10 @@ void on_ctrl_recv_callback(spdylay_session *session, spdylay_frame_type type, downstream->set_request_state(Downstream::MSG_COMPLETE); } + if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { + return; + } + upstream->start_downstream(downstream); break; @@ -574,6 +600,11 @@ int SpdyUpstream::downstream_read(DownstreamConnection *dconn) { if (rv == SHRPX_ERR_EOF) { return downstream_eof(dconn); } + if (rv == SHRPX_ERR_DCONN_CANCELED) { + downstream->pop_downstream_connection(); + handler_->signal_write(); + return 0; + } if (rv != 0) { if (rv != SHRPX_ERR_NETWORK) { if (LOG_ENABLED(INFO)) { @@ -773,6 +804,70 @@ ssize_t spdy_data_read_callback(spdylay_session *session, int32_t stream_id, } } // namespace +int SpdyUpstream::send_reply(Downstream *downstream, const uint8_t *body, + size_t bodylen) { + int rv; + + spdylay_data_provider data_prd, *data_prd_ptr = nullptr; + if (bodylen) { + data_prd.source.ptr = downstream; + data_prd.read_callback = spdy_data_read_callback; + data_prd_ptr = &data_prd; + } + + auto status_string = + http2::get_status_string(downstream->get_response_http_status()); + + auto &headers = downstream->get_response_headers(); + + auto nva = std::vector(); + // 3 for :status, :version and server + nva.reserve(3 + headers.size()); + + nva.push_back(":status"); + nva.push_back(status_string.c_str()); + nva.push_back(":version"); + nva.push_back("HTTP/1.1"); + + for (auto &kv : headers) { + if (kv.name.empty() || kv.name[0] == ':') { + continue; + } + switch (kv.token) { + case http2::HD_CONNECTION: + case http2::HD_KEEP_ALIVE: + case http2::HD_PROXY_CONNECTION: + case http2::HD_TRANSFER_ENCODING: + continue; + } + nva.push_back(kv.name.c_str()); + nva.push_back(kv.value.c_str()); + } + + if (!downstream->get_response_header(http2::HD_SERVER)) { + nva.push_back("server"); + nva.push_back(get_config()->server_name); + } + + nva.push_back(nullptr); + + rv = spdylay_submit_response(session_, downstream->get_stream_id(), + nva.data(), data_prd_ptr); + if (rv < SPDYLAY_ERR_FATAL) { + ULOG(FATAL, this) << "spdylay_submit_response() failed: " + << spdylay_strerror(rv); + return -1; + } + + auto buf = downstream->get_response_buf(); + + buf->append(body, bodylen); + + downstream->set_response_state(Downstream::MSG_COMPLETE); + + return 0; +} + int SpdyUpstream::error_reply(Downstream *downstream, unsigned int status_code) { int rv; @@ -845,6 +940,22 @@ int SpdyUpstream::on_downstream_header_complete(Downstream *downstream) { return 0; } +#ifdef HAVE_MRUBY + auto worker = handler_->get_worker(); + auto mruby_ctx = worker->get_mruby_context(); + + if (mruby_ctx->run_on_response_proc(downstream) != 0) { + if (error_reply(downstream, 500) != 0) { + return -1; + } + return -1; + } + + if (downstream->get_response_state() == Downstream::MSG_COMPLETE) { + return -1; + } +#endif // HAVE_MRUBY + if (LOG_ENABLED(INFO)) { DLOG(INFO, downstream) << "HTTP response header completed"; } @@ -1092,4 +1203,9 @@ int SpdyUpstream::on_downstream_reset(bool no_retry) { return 0; } +int SpdyUpstream::initiate_push(Downstream *downstream, const char *uri, + size_t len) { + return 0; +} + } // namespace shrpx diff --git a/src/shrpx_spdy_upstream.h b/src/shrpx_spdy_upstream.h index 2e4aec47..b39f885f 100644 --- a/src/shrpx_spdy_upstream.h +++ b/src/shrpx_spdy_upstream.h @@ -74,6 +74,11 @@ public: virtual void on_handler_delete(); virtual int on_downstream_reset(bool no_retry); + virtual int send_reply(Downstream *downstream, const uint8_t *body, + size_t bodylen); + virtual int initiate_push(Downstream *downstream, const char *uri, + size_t len); + bool get_flow_control() const; int consume(int32_t stream_id, size_t len); diff --git a/src/shrpx_upstream.h b/src/shrpx_upstream.h index fe9a673d..e367d351 100644 --- a/src/shrpx_upstream.h +++ b/src/shrpx_upstream.h @@ -62,6 +62,11 @@ public: virtual void pause_read(IOCtrlReason reason) = 0; virtual int resume_read(IOCtrlReason reason, Downstream *downstream, size_t consumed) = 0; + virtual int send_reply(Downstream *downstream, const uint8_t *body, + size_t bodylen) = 0; + + virtual int initiate_push(Downstream *downstream, const char *uri, + size_t len) = 0; }; } // namespace shrpx diff --git a/src/shrpx_worker.cc b/src/shrpx_worker.cc index 58bfb633..db236649 100644 --- a/src/shrpx_worker.cc +++ b/src/shrpx_worker.cc @@ -37,6 +37,9 @@ #include "shrpx_log_config.h" #include "shrpx_connect_blocker.h" #include "shrpx_memcached_dispatcher.h" +#ifdef HAVE_MRUBY +#include "shrpx_mruby.h" +#endif // HAVE_MRUBY #include "util.h" #include "template.h" @@ -264,4 +267,19 @@ MemcachedDispatcher *Worker::get_session_cache_memcached_dispatcher() { return session_cache_memcached_dispatcher_.get(); } +#ifdef HAVE_MRUBY +int Worker::create_mruby_context() { + mruby_ctx_ = mruby::create_mruby_context(); + if (!mruby_ctx_) { + return -1; + } + + return 0; +} + +mruby::MRubyContext *Worker::get_mruby_context() const { + return mruby_ctx_.get(); +} +#endif // HAVE_MRUBY + } // namespace shrpx diff --git a/src/shrpx_worker.h b/src/shrpx_worker.h index 3613b0d5..6037683d 100644 --- a/src/shrpx_worker.h +++ b/src/shrpx_worker.h @@ -51,6 +51,14 @@ class Http2Session; class ConnectBlocker; class MemcachedDispatcher; +#ifdef HAVE_MRUBY +namespace mruby { + +class MRubyContext; + +} // namespace mruby +#endif // HAVE_MRUBY + namespace ssl { class CertLookupTree; } // namespace ssl @@ -124,6 +132,12 @@ public: MemcachedDispatcher *get_session_cache_memcached_dispatcher(); +#ifdef HAVE_MRUBY + int create_mruby_context(); + + mruby::MRubyContext *get_mruby_context() const; +#endif // HAVE_MRUBY + private: #ifndef NOTHREADS std::future fut_; @@ -137,6 +151,9 @@ private: WorkerStat worker_stat_; std::vector dgrps_; std::unique_ptr session_cache_memcached_dispatcher_; +#ifdef HAVE_MRUBY + std::unique_ptr mruby_ctx_; +#endif // HAVE_MRUBY struct ev_loop *loop_; // Following fields are shared across threads if diff --git a/third-party/Makefile.am b/third-party/Makefile.am index 8098e32e..cdabf30c 100644 --- a/third-party/Makefile.am +++ b/third-party/Makefile.am @@ -30,5 +30,23 @@ libhttp_parser_la_SOURCES = \ http-parser/http_parser.c \ http-parser/http_parser.h +if HAVE_MRUBY + +EXTRA_DIST = build_config.rb mruby/* + +.PHONY: all-local clean mruby + +mruby: + MRUBY_CONFIG="${srcdir}/build_config.rb" \ + BUILD_DIR="${abs_builddir}/mruby/build" \ + CC="${CC}" CXX="${CXX}" LD="${LD}" \ + "${srcdir}/mruby/minirake" -f "${srcdir}/mruby/Rakefile" + +all-local: mruby + +clean-local: + -rm -rf "${abs_builddir}/mruby/build" + +endif # HAVE_MRUBY endif # ENABLE_THIRD_PARTY diff --git a/third-party/build_config.rb b/third-party/build_config.rb new file mode 100644 index 00000000..7ddd2e9b --- /dev/null +++ b/third-party/build_config.rb @@ -0,0 +1,14 @@ +MRuby::Build.new do |conf| + # TODO use same compilers configured in configure script + toolchain :clang + + # C++ project needs this. Without this, mruby exception does not + # properly destory C++ object allocated on stack. + conf.enable_cxx_abi + + conf.build_dir = ENV['BUILD_DIR'] + + # include the default GEMs + conf.gembox 'default' + conf.gem :core => 'mruby-eval' +end diff --git a/third-party/mruby b/third-party/mruby new file mode 160000 index 00000000..1cbbb7e1 --- /dev/null +++ b/third-party/mruby @@ -0,0 +1 @@ +Subproject commit 1cbbb7e11c02d381a6b76aeebae8db0f54ae9baf