Commit 6d46249b authored by Tatsuhiro Tsujikawa's avatar Tatsuhiro Tsujikawa

Merge branch 'nghttpx-mruby'

parents 8f41accf 1b63e6d4
......@@ -6,6 +6,7 @@
*.lo
*.m4
*.o
*.pyc
.deps/
.libs/
INSTALL
......@@ -31,10 +32,10 @@ test-driver
# test logs generated by `make check`
*.log
*.trs
lib/MSVC_obj/
_VC_ROOT/
.depend.MSVC
*.pyd
*.egg-info/
python/nghttp2.c
lib/MSVC_obj/
_VC_ROOT/
.depend.MSVC
*.pyd
*.egg-info/
python/nghttp2.c
[submodule "third-party/mruby"]
path = third-party/mruby
url = https://github.com/mruby/mruby
......@@ -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
......@@ -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
......
......@@ -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}
......
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.
# -*- 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__)
This diff is collapsed.
......@@ -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']
......
......@@ -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
--------
......
......@@ -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",
]
......
......@@ -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
......
......@@ -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)
}
}
......@@ -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)
}
}
......@@ -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)
}
}
Nghttpx.run do |env|
env.req.set_header "User-Agent", "mruby"
end
Nghttpx.run do |env|
env.resp.set_header "Alpha", "bravo"
end
Nghttpx.run do |env|
resp = env.resp
resp.clear_headers
resp.status = 404
resp.add_header "from", "mruby"
resp.return "Hello World"
end
......@@ -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
......@@ -130,12 +130,29 @@ if HAVE_SPDYLAY
NGHTTPX_SRCS += shrpx_spdy_upstream.cc shrpx_spdy_upstream.h
endif # HAVE_SPDYLAY
if HAVE_MRUBY
NGHTTPX_SRCS += \
shrpx_mruby.cc shrpx_mruby.h \
shrpx_mruby_module.cc shrpx_mruby_module.h \
shrpx_mruby_module_env.cc shrpx_mruby_module_env.h \
shrpx_mruby_module_request.cc shrpx_mruby_module_request.h \
shrpx_mruby_module_response.cc shrpx_mruby_module_response.h
endif # HAVE_MRUBY
noinst_LIBRARIES = libnghttpx.a
libnghttpx_a_SOURCES = ${NGHTTPX_SRCS}
libnghttpx_a_CPPFLAGS = ${AM_CPPFLAGS}
nghttpx_SOURCES = shrpx.cc shrpx.h
nghttpx_CPPFLAGS = ${libnghttpx_a_CPPFLAGS}
nghttpx_LDADD = libnghttpx.a ${LDADD}
if HAVE_MRUBY
libnghttpx_a_CPPFLAGS += \
-I${top_srcdir}/third-party/mruby/include @LIBMRUBY_CFLAGS@
nghttpx_LDADD += -L${top_builddir}/third-party/mruby/build/lib @LIBMRUBY_LIBS@
endif # HAVE_MRUBY
if HAVE_CUNIT
check_PROGRAMS += nghttpx-unittest
nghttpx_unittest_SOURCES = shrpx-unittest.cc \
......@@ -148,10 +165,17 @@ nghttpx_unittest_SOURCES = shrpx-unittest.cc \
nghttp2_gzip.c nghttp2_gzip.h \
buffer_test.cc buffer_test.h \
memchunk_test.cc memchunk_test.h
nghttpx_unittest_CPPFLAGS = ${AM_CPPFLAGS}\
nghttpx_unittest_CPPFLAGS = ${AM_CPPFLAGS} \
-DNGHTTP2_TESTS_DIR=\"$(top_srcdir)/tests\"
nghttpx_unittest_LDADD = libnghttpx.a ${LDADD} @CUNIT_LIBS@ @TESTLDADD@
if HAVE_MRUBY
nghttpx_unittest_CPPFLAGS += \
-I${top_srcdir}/third-party/mruby/include @LIBMRUBY_CFLAGS@
nghttpx_unittest_LDADD += \
-L${top_builddir}/third-party/mruby/build/lib @LIBMRUBY_LIBS@
endif # HAVE_MRUBY
TESTS += nghttpx-unittest
endif # HAVE_CUNIT
......
......@@ -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
......@@ -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
......
......@@ -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
......@@ -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
......
......@@ -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",
......
......@@ -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";
......
......@@ -739,41 +739,23 @@ namespace {
// HttpDownstreamConnection::push_request_headers(), but vastly
// simplified since we only care about absolute URI.
std::string construct_absolute_request_uri(Downstream *downstream) {
const char *authority = nullptr, *host = nullptr;
if (!downstream->get_request_http2_authority().empty()) {
authority = downstream->get_request_http2_authority().c_str();
}
auto h = downstream->get_request_header(http2::HD_HOST);
if (h) {
host = h->value.c_str();
}
if (!authority && !host) {
auto &authority = downstream->get_request_http2_authority();
if (authority.empty()) {
return downstream->get_request_path();
}
std::string uri;
if (downstream->get_request_http2_scheme().empty()) {
auto &scheme = downstream->get_request_http2_scheme();
if (scheme.empty()) {
// We may have to log the request which lacks scheme (e.g.,
// http/1.1 with origin form).
uri += "http://";
} else {
uri += downstream->get_request_http2_scheme();
uri += scheme;
uri += "://";
}
if (authority) {
uri += authority;
} else {
uri += host;
}
uri += authority;
uri += downstream->get_request_path();
// Server-wide OPTIONS takes following form in proxy request:
//
// OPTIONS http://example.org HTTP/1.1
//
// Notice that no slash after authority. See
// http://tools.ietf.org/html/rfc7230#section-5.3.4
if (downstream->get_request_path() != "*") {
uri += downstream->get_request_path();
}
return uri;
}
} // namespace
......@@ -787,12 +769,15 @@ void ClientHandler::write_accesslog(Downstream *downstream) {
downstream, ipaddr_.c_str(),
http2::to_method_string(downstream->get_request_method()),
(downstream->get_request_method() != HTTP_CONNECT &&
(get_config()->http2_proxy || get_config()->client_proxy))
? construct_absolute_request_uri(downstream).c_str()
: downstream->get_request_path().empty()
? downstream->get_request_http2_authority().c_str()
: downstream->get_request_path().c_str(),
downstream->get_request_method() == HTTP_CONNECT
? downstream->get_request_http2_authority().c_str()
: (get_config()->http2_proxy || get_config()->client_proxy)
? construct_absolute_request_uri(downstream).c_str()
: downstream->get_request_path().empty()
? downstream->get_request_method() == HTTP_OPTIONS
? "*"
: "-"
: downstream->get_request_path().c_str(),
alpn_.c_str(),
nghttp2::ssl::get_tls_session_info(&tls_info, conn_.tls.ssl),
......
......@@ -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";
......
......@@ -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;
......
......@@ -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() {
......
......@@ -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();
......
......@@ -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 =
......
......@@ -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);
......
......@@ -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
......
......@@ -251,10 +251,10 @@ int Http2DownstreamConnection::push_request_headers() {
downstream_->set_request_pending(false);
auto method = downstream_->get_request_method();
auto no_host_rewrite = get_config()->no_host_rewrite ||
get_config()->http2_proxy ||
get_config()->client_proxy ||
downstream_->get_request_method() == HTTP_CONNECT;
get_config()->client_proxy || method == HTTP_CONNECT;
// http2session_ has already in CONNECTED state, so we can get
// addr_idx here.
......@@ -265,37 +265,21 @@ int Http2DownstreamConnection::push_request_headers() {
.addrs[addr_idx]
.hostport.get();
const char *authority = nullptr, *host = nullptr;
if (!no_host_rewrite) {
if (!downstream_->get_request_http2_authority().empty()) {
authority = downstream_hostport;
}
if (downstream_->get_request_header(http2::HD_HOST)) {
host = downstream_hostport;
}
} else {
if (!downstream_->get_request_http2_authority().empty()) {
authority = downstream_->get_request_http2_authority().c_str();
}
auto h = downstream_->get_request_header(http2::HD_HOST);
if (h) {
host = h->value.c_str();
}
// For HTTP/1.0 request, there is no authority in request. In that
// case, we use backend server's host nonetheless.
const char *authority = downstream_hostport;
auto &req_authority = downstream_->get_request_http2_authority();
if (no_host_rewrite && !req_authority.empty()) {
authority = req_authority.c_str();
}
if (!authority && !host) {
// upstream is HTTP/1.0. We use backend server's host
// nonetheless.
host = downstream_hostport;
if (!authority) {
authority = downstream_hostport;
}
if (authority) {
downstream_->set_request_downstream_host(authority);
} else {
downstream_->set_request_downstream_host(host);
}
downstream_->set_request_downstream_host(authority);
size_t nheader = downstream_->get_request_headers().size();
auto nheader = downstream_->get_request_headers().size();
Headers cookies;
if (!get_config()->http2_no_cookie_crumbling) {
......@@ -306,7 +290,7 @@ int Http2DownstreamConnection::push_request_headers() {
// 1. :method
// 2. :scheme
// 3. :path
// 4. :authority or host (at least either of them exists)
// 4. :authority
// 5. via (optional)
// 6. x-forwarded-for (optional)
// 7. x-forwarded-proto (optional)
......@@ -315,33 +299,23 @@ int Http2DownstreamConnection::push_request_headers() {
nva.reserve(nheader + 8 + cookies.size() +
get_config()->add_request_headers.size());
nva.push_back(http2::make_nv_lc(
":method", http2::to_method_string(downstream_->get_request_method())));
nva.push_back(http2::make_nv_lc(":method", http2::to_method_string(method)));
auto &scheme = downstream_->get_request_http2_scheme();
if (downstream_->get_request_method() == HTTP_CONNECT) {
if (authority) {
nva.push_back(http2::make_nv_lc(":authority", authority));
} else {
nva.push_back(
http2::make_nv_ls(":authority", downstream_->get_request_path()));
}
} else {
nva.push_back(http2::make_nv_lc(":authority", authority));
if (method != HTTP_CONNECT) {
assert(!scheme.empty());
nva.push_back(http2::make_nv_ls(":scheme", scheme));
if (authority) {
nva.push_back(http2::make_nv_lc(":authority", authority));
auto &path = downstream_->get_request_path();
if (method == HTTP_OPTIONS && path.empty()) {
nva.push_back(http2::make_nv_ll(":path", "*"));
} else {
nva.push_back(http2::make_nv_ls(":path", path));
}
nva.push_back(http2::make_nv_ls(":path", downstream_->get_request_path()));
}
// only emit host header field if :authority is not emitted. They
// both must be the same value.
if (!authority && host) {
nva.push_back(http2::make_nv_lc("host", host));
}
http2::copy_headers_to_nva(nva, downstream_->get_request_headers());
......
......@@ -903,9 +903,15 @@ int on_response_headers(Http2Session *http2session, Downstream *downstream,
rv = upstream->on_downstream_header_complete(downstream);
if (rv != 0) {
http2session->submit_rst_stream(frame->hd.stream_id,
NGHTTP2_PROTOCOL_ERROR);
downstream->set_response_state(Downstream::MSG_RESET);
// Handling early return (in other words, response was hijacked by
// mruby scripting).
if (downstream->get_response_state() == Downstream::MSG_COMPLETE) {
http2session->submit_rst_stream(frame->hd.stream_id, NGHTTP2_CANCEL);
} else {
http2session->submit_rst_stream(frame->hd.stream_id,
NGHTTP2_INTERNAL_ERROR);
downstream->set_response_state(Downstream::MSG_RESET);
}
}
return 0;
......
This diff is collapsed.
......@@ -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
......
......@@ -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 (!authority && !host) {
// upstream is HTTP/1.0. We use backend server's host
// nonetheless.
host = downstream_hostport;
if (no_host_rewrite && !req_authority.empty()) {
authority = req_authority.c_str();
}
if (authority) {
downstream_->set_request_downstream_host(authority);
} else {
downstream_->set_request_downstream_host(host);
}
downstream_->set_request_downstream_host(authority);
downstream_->assemble_request_cookie();
// Assume that method and request path do not contain \r\n.
std::string hdrs = http2::to_method_string(downstream_->get_request_method());
std::string hdrs = http2::to_method_string(method);
hdrs += ' ';
auto &scheme = downstream_->get_request_http2_scheme();
auto &path = downstream_->get_request_path();
if (connect_method) {
if (authority) {
hdrs += authority;
} else {
hdrs += downstream_->get_request_path();
}
hdrs += authority;
} else if (get_config()->http2_proxy || get_config()->client_proxy) {
// Construct absolute-form request target because we are going to
// send a request to a HTTP/1 proxy.
assert(!scheme.empty());
hdrs += scheme;
hdrs += "://";
if (authority) {
hdrs += authority;
} else {
hdrs += host;
}
// Server-wide OPTIONS takes following form in proxy request:
//
// OPTIONS http://example.org HTTP/1.1
//
// Notice that no slash after authority. See
// http://tools.ietf.org/html/rfc7230#section-5.3.4
if (downstream_->get_request_path() != "*") {
hdrs += downstream_->get_request_path();
}
} else {
// No proxy case.
hdrs += downstream_->get_request_path();
}
hdrs += " HTTP/1.1\r\nHost: ";
if (authority) {
hdrs += authority;
hdrs += path;
} else if (method == HTTP_OPTIONS && path.empty()) {
// Server-wide OPTIONS
hdrs += "*";
} else {
hdrs += host;
hdrs += path;
}
hdrs += " HTTP/1.1\r\nHost: ";
hdrs += authority;
hdrs += "\r\n";
http2::build_http1_headers_from_headers(hdrs,
......@@ -774,6 +739,12 @@ int HttpDownstreamConnection::on_read() {
auto htperr = HTTP_PARSER_ERRNO(&response_htp_);
if (htperr != HPE_OK) {
// Handling early return (in other words, response was hijacked
// by mruby scripting).
if (downstream_->get_response_state() == Downstream::MSG_COMPLETE) {
return SHRPX_ERR_DCONN_CANCELED;
}
if (LOG_ENABLED(INFO)) {
DCLOG(INFO, this) << "HTTP parser failure: "
<< "(" << http_errno_name(htperr) << ") "
......
This diff is collapsed.
......@@ -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;
......
/*
* 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
/*
* 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
/*
* 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
/*
* 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
/*
* 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
/*
* 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
This diff is collapsed.
/*
* 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
/*
* 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
/*
* 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
......@@ -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
......@@ -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);
......
......@@ -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
......
......@@ -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
......@@ -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
......
......@@ -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
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
Subproject commit 1cbbb7e11c02d381a6b76aeebae8db0f54ae9baf
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment