Merge branch 'nghttpx-mruby'

This commit is contained in:
Tatsuhiro Tsujikawa 2015-09-06 16:39:21 +09:00
commit 6d46249b7b
59 changed files with 3884 additions and 261 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@
*.lo
*.m4
*.o
*.pyc
.deps/
.libs/
INSTALL

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "third-party/mruby"]
path = third-party/mruby
url = https://github.com/mruby/mruby

View File

@ -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

View File

@ -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
<https://github.com/mruby/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

View File

@ -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}

View File

@ -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.

View File

@ -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__)

View File

@ -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)

View File

@ -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']

View File

@ -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
--------

View File

@ -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",
]

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -0,0 +1,3 @@
Nghttpx.run do |env|
env.req.set_header "User-Agent", "mruby"
end

View File

@ -0,0 +1,3 @@
Nghttpx.run do |env|
env.resp.set_header "Alpha", "bravo"
end

View File

@ -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

View File

@ -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

View File

@ -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 \
@ -152,6 +169,13 @@ 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

View File

@ -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 <typename InputIt> 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<http_method>(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

View File

@ -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<const char *, const char *> 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

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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 <USER>. This option is intended to
be used to drop root privileges.
Scripting:
--request-phase-file=<PATH>
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=<PATH>
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=<PATH>
Load configuration from <PATH>.
@ -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";

View File

@ -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;
}
// 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,11 +769,14 @@ 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))
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_http2_authority().c_str()
? downstream->get_request_method() == HTTP_OPTIONS
? "*"
: "-"
: downstream->get_request_path().c_str(),
alpn_.c_str(),

View File

@ -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";

View File

@ -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<char[]> user;
std::unique_ptr<char[]> session_cache_memcached_host;
std::unique_ptr<char[]> tls_ticket_key_memcached_host;
std::unique_ptr<char[]> request_phase_file;
std::unique_ptr<char[]> response_phase_file;
FILE *http2_upstream_dump_request_header;
FILE *http2_upstream_dump_response_header;
nghttp2_session_callbacks *http2_upstream_callbacks;

View File

@ -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<Worker>(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<Worker>(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() {

View File

@ -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<TicketKeys> &ticket_keys);
void worker_reopen_log_files();

View File

@ -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 =

View File

@ -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);

View File

@ -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

View File

@ -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()) {
// 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) {
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();
}
}
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);
}
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 {
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());

View File

@ -903,10 +903,16 @@ int on_response_headers(Http2Session *http2session, Downstream *downstream,
rv = upstream->on_downstream_header_complete(downstream);
if (rv != 0) {
// 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_PROTOCOL_ERROR);
NGHTTP2_INTERNAL_ERROR);
downstream->set_response_state(Downstream::MSG_RESET);
}
}
return 0;
}

View File

@ -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<nghttp2_nv>();
// 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<nghttp2_nv>();
// 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,81 +1593,32 @@ 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;
}
}
if (scheme.empty()) {
scheme = downstream->get_request_http2_scheme();
@ -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

View File

@ -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

View File

@ -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_->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();
}
} 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;
hdrs += path;
} else if (method == HTTP_OPTIONS && path.empty()) {
// Server-wide OPTIONS
hdrs += "*";
} 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();
}
} 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 += "\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) << ") "

View File

@ -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<Downstream>(upstream, handler->get_mcpool(), 0, 0));
auto downstream =
make_unique<Downstream>(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);
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;
}
if (method == HTTP_OPTIONS && path == "*") {
downstream->set_request_path("");
} else {
downstream->set_request_path(
http2::rewrite_clean_path(std::begin(uri), std::end(uri)));
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,6 +530,8 @@ int HttpsUpstream::on_read() {
if (htperr == HPE_INVALID_METHOD) {
status_code = 501;
} else if (downstream) {
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() ==
@ -480,6 +540,7 @@ int HttpsUpstream::on_read() {
} 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

View File

@ -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;

203
src/shrpx_mruby.cc Normal file
View File

@ -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 <mruby/compile.h>
#include <mruby/string.h>
#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<MRubyContext> 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<MRubyContext>(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<MRubyContext>(mrb, req_proc, res_proc);
}
mrb_sym intern_ptr(mrb_state *mrb, void *ptr) {
auto p = reinterpret_cast<uintptr_t>(ptr);
return mrb_intern(mrb, reinterpret_cast<const char *>(&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

88
src/shrpx_mruby.h Normal file
View File

@ -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 <memory>
#include <mruby.h>
#include <mruby/proc.h>
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<MRubyContext> 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

128
src/shrpx_mruby_module.cc Normal file
View File

@ -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 <array>
#include <mruby/variable.h>
#include <mruby/string.h>
#include <mruby/hash.h>
#include <mruby/array.h>
#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<RObject *>(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<RObject *>(module), env_sym, env);
}
std::array<mrb_value, 1> 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<RObject *>(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

52
src/shrpx_mruby_module.h Normal file
View File

@ -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 <mruby.h>
#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

View File

@ -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 <mruby/variable.h>
#include <mruby/string.h>
#include <mruby/hash.h>
#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<MRubyAssocData *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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

View File

@ -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 <mruby.h>
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

View File

@ -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 <mruby/variable.h>
#include <mruby/string.h>
#include <mruby/hash.h>
#include <mruby/array.h>
#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<MRubyAssocData *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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<const uint8_t *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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

View File

@ -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 <mruby.h>
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

View File

@ -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 <mruby/variable.h>
#include <mruby/string.h>
#include <mruby/hash.h>
#include <mruby/array.h>
#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<MRubyAssocData *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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<MRubyAssocData *>(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<const uint8_t *>(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

View File

@ -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 <mruby.h>
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

View File

@ -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<const char *>();
// 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

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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<void> fut_;
@ -137,6 +151,9 @@ private:
WorkerStat worker_stat_;
std::vector<DownstreamGroup> dgrps_;
std::unique_ptr<MemcachedDispatcher> session_cache_memcached_dispatcher_;
#ifdef HAVE_MRUBY
std::unique_ptr<mruby::MRubyContext> mruby_ctx_;
#endif // HAVE_MRUBY
struct ev_loop *loop_;
// Following fields are shared across threads if

View File

@ -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

14
third-party/build_config.rb vendored Normal file
View File

@ -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

1
third-party/mruby vendored Submodule

@ -0,0 +1 @@
Subproject commit 1cbbb7e11c02d381a6b76aeebae8db0f54ae9baf