Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM openresty/openresty:1.31.1.1-alpine-fat

RUN apk add --no-cache curl perl bash wget git perl-dev libarchive-tools nodejs; \
ln -s /usr/bin/bsdtar /usr/bin/tar

RUN curl -s -L https://cpanmin.us | perl - App::cpanminus > /bin/cpanm && chmod +x /bin/cpanm

RUN cpanm -q -n Test::Nginx

RUN /usr/local/openresty/luajit/bin/luarocks install luacov && \
/usr/local/openresty/luajit/bin/luarocks install lua-resty-openssl && \
/usr/local/openresty/luajit/bin/luarocks install luacheck
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ The options table has the following fields:
* `ssl_send_status_req`: option as per [OpenResty docs](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake)
* `ssl_client_cert`: will be passed to `tcpsock:setclientcert`. Requires `ngx_lua_http_module` >= v0.10.23.
* `ssl_client_priv_key`: as above.
* `ssl_trusted_store`: a custom trusted CA store (cdata `X509_STORE*`) to verify the server certificate against, passed to `tcpsock:settrustedstore`. Requires `ngx_lua_http_module` >= v0.10.31rc1

## set\_timeout

Expand Down
35 changes: 35 additions & 0 deletions lib/resty/http_connect.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ local ngx_re_gmatch = ngx.re.gmatch
local ngx_re_sub = ngx.re.sub
local ngx_re_find = ngx.re.find
local ngx_log = ngx.log
local ngx_md5 = ngx.md5
local ngx_WARN = ngx.WARN
local ngx_DEBUG = ngx.DEBUG
local to_hex = require("resty.string").to_hex
Expand Down Expand Up @@ -53,6 +54,11 @@ client:connect {
ssl_client_cert = nil,
ssl_client_priv_key = nil,

-- Custom trusted CA store (cdata `X509_STORE*`), passed to
-- `tcpsock:settrustedstore`. Requires a cosocket build with
-- `settrustedstore` support.
ssl_trusted_store = nil,

proxy_opts, -- proxy opts, defaults to global proxy options
}
]]
Expand Down Expand Up @@ -81,6 +87,7 @@ local function connect(self, options)
-- ssl settings
local ssl, ssl_reused_session, ssl_server_name
local ssl_verify, ssl_send_status_req, ssl_client_cert, ssl_client_priv_key
local ssl_trusted_store
if request_scheme == "https" then
ssl = true
ssl_reused_session = options.ssl_reused_session
Expand All @@ -92,6 +99,7 @@ local function connect(self, options)
end
ssl_client_cert = options.ssl_client_cert
ssl_client_priv_key = options.ssl_client_priv_key
ssl_trusted_store = options.ssl_trusted_store
end

-- proxy related settings
Expand Down Expand Up @@ -233,6 +241,22 @@ local function connect(self, options)
cert_hash = to_hex(cert_hash) -- convert to hex so that it's printable
end

-- Validate ssl_trusted_store
if ssl and ssl_trusted_store then
if type(ssl_trusted_store) ~= "cdata" then
return nil, "bad ssl_trusted_store: cdata expected, got " .. type(ssl_trusted_store)
end

if type(sock.setclientcert) ~= "function" then
return nil, "cannot use ssl_trusted_store without settrustedstore support"
end
end

local ssl_trusted_store_id
if ssl_trusted_store then
ssl_trusted_store_id = ngx_md5(tostring(ssl_trusted_store))
end

-- construct a poolname unique within proxy and ssl info
if not poolname then
poolname = (request_scheme or "")
Expand All @@ -244,6 +268,7 @@ local function connect(self, options)
.. ":" .. (proxy_uri or "")
.. ":" .. (request_scheme == "https" and proxy_authorization or "")
.. ":" .. (cert_hash or "")
.. ":" .. tostring(ssl_trusted_store_id or "")
-- in the above we only add the 'proxy_authorization' as part of the poolname
-- when the request is https. Because in that case the CONNECT request (which
-- carries the authorization header) is part of the connect procedure, whereas
Expand Down Expand Up @@ -315,11 +340,21 @@ local function connect(self, options)
else
ok, err = sock:setclientcert(ssl_client_cert, ssl_client_priv_key)
if not ok then
self:close()
return nil, "could not set client certificate: " .. err
end
end
end

-- Custom CA trusted store support
if ssl_trusted_store then
ok, err = sock:settrustedstore(ssl_trusted_store)
if not ok then
self:close()
return nil, "could not set trusted store: " .. err
end
end

ssl_session, err = sock:sslhandshake(ssl_reused_session, ssl_server_name, ssl_verify, ssl_send_status_req)
if not ssl_session then
self:close()
Expand Down
8 changes: 4 additions & 4 deletions lua-resty-http-0.17.2-0.rockspec
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package = "lua-resty-http"
package = "3scale-lua-resty-http"
version = "0.17.2-0"
source = {
url = "git://github.com/ledgetech/lua-resty-http",
url = "git@github.com/3scale/lua-resty-http.git",
tag = "v0.17.2"
}
description = {
summary = "Lua HTTP client cosocket driver for OpenResty / ngx_lua.",
homepage = "https://github.com/ledgetech/lua-resty-http",
homepage = "https://github.com/3scale/lua-resty-http",
license = "2-clause BSD",
maintainer = "James Hurst <james@pintsized.co.uk>"
maintainer = "3scale"
}
dependencies = {
"lua >= 5.1"
Expand Down
232 changes: 232 additions & 0 deletions t/21-ssl-trusted-store.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
use Test::Nginx::Socket::Lua 'no_plan';


log_level 'debug';

no_long_string();

sub read_file {
my $infile = shift;
open my $in, $infile
or die "cannot open $infile for reading: $!";
my $cert = do { local $/; <$in> };
close $in;
$cert;
}

our $TestCert = read_file("t/cert/test.crt");
our $TestKey = read_file("t/cert/test.key");

our $HtmlDir = html_dir;

use Cwd qw(cwd);
my $pwd = cwd();

our $http_config = <<"_EOC_";
lua_package_path "$pwd/lib/?.lua;/usr/local/share/lua/5.1/?.lua;;";
server {
listen unix:$::HtmlDir/ssl.sock ssl;

ssl_certificate $::HtmlDir/test.crt;
ssl_certificate_key $::HtmlDir/test.key;
server_tokens off;
server_name example.com;

location / {
echo -n "hello";
}
}
_EOC_

our $user_files = <<"_EOC_";
>>> test.crt
$::TestCert
>>> test.key
$::TestKey
_EOC_

run_tests();

__DATA__

=== TEST 1: ssl_trusted_store rejects non-cdata values
--- http_config eval: $::http_config
--- config eval
"
lua_ssl_trusted_certificate $::HtmlDir/test.crt;
location /t {
content_by_lua_block {
local httpc = assert(require('resty.http').new())

local ok, err = httpc:connect {
scheme = 'https',
host = 'unix:$::HtmlDir/ssl.sock',
ssl_trusted_store = 'not_a_cdata',
}

if not ok then
ngx.status = 400
ngx.say(err)
else
ngx.say('unexpected success')
end
}
}
"
--- user_files eval: $::user_files
--- request
GET /t
--- error_code: 400
--- response_body
bad ssl_trusted_store: cdata expected, got string
--- no_error_log
[error]
[warn]


=== TEST 2: ssl_trusted_store rejects table values
--- http_config eval: $::http_config
--- config eval
"
lua_ssl_trusted_certificate $::HtmlDir/test.crt;
location /t {
content_by_lua_block {
local httpc = assert(require('resty.http').new())

local ok, err = httpc:connect {
scheme = 'https',
host = 'unix:$::HtmlDir/ssl.sock',
ssl_trusted_store = {},
}

if not ok then
ngx.status = 400
ngx.say(err)
else
ngx.say('unexpected success')
end
}
}
"
--- user_files eval: $::user_files
--- request
GET /t
--- error_code: 400
--- response_body
bad ssl_trusted_store: cdata expected, got table
--- no_error_log
[error]
[warn]


=== TEST 3: ssl_trusted_store is ignored for non-https schemes
--- http_config eval: $::http_config
--- config eval
"
location /backend {
echo -n 'ok';
}

location /t {
content_by_lua_block {
local httpc = assert(require('resty.http').new())

local ok, err = httpc:connect {
scheme = 'http',
host = '127.0.0.1',
port = \$TEST_NGINX_SERVER_PORT,
ssl_trusted_store = 'should_be_ignored',
}

if not ok then
ngx.say('connect failed: ' .. err)
return
end

local res, err = httpc:request {
method = 'GET',
path = '/backend',
}

if not res then
ngx.say('request failed: ' .. err)
return
end

ngx.say(res:read_body())
httpc:close()
}
}
"
--- user_files eval: $::user_files
--- request
GET /t
--- error_code: 200
--- response_body
ok
--- no_error_log
[error]
[warn]


=== TEST 4: same ssl_trusted_store reuses pool, different store gets a new pool
--- http_config eval: $::http_config
--- config eval
"
lua_ssl_trusted_certificate $::HtmlDir/test.crt;
location /t {
content_by_lua_block {
local x509_store = require('resty.openssl.x509.store')

local store1 = assert(x509_store.new())
local store2 = assert(x509_store.new())

-- First connection with store1
local httpc1 = assert(require('resty.http').new())
local ok, err = httpc1:connect {
scheme = 'https',
host = 'unix:$::HtmlDir/ssl.sock',
ssl_trusted_store = store1.ctx,
ssl_verify = false,
}
assert(ok and not err, 'connect 1 failed: ' .. (err or ''))
httpc1:set_keepalive()

-- Second connection with the same store1 — should reuse the pool
local httpc2 = assert(require('resty.http').new())
ok, err = httpc2:connect {
scheme = 'https',
host = 'unix:$::HtmlDir/ssl.sock',
ssl_trusted_store = store1.ctx,
ssl_verify = false,
}
assert(ok and not err, 'connect 2 failed: ' .. (err or ''))
ngx.say('same store reused: ', httpc2:get_reused_times())
httpc2:set_keepalive()

-- Third connection with a different store2 — should NOT reuse the pool
local httpc3 = assert(require('resty.http').new())
ok, err = httpc3:connect {
scheme = 'https',
host = 'unix:$::HtmlDir/ssl.sock',
ssl_trusted_store = store2.ctx,
ssl_verify = false,
}
assert(ok and not err, 'connect 3 failed: ' .. (err or ''))
ngx.say('different store reused: ', httpc3:get_reused_times())
httpc3:close()
}
}
"
--- user_files eval: $::user_files
--- request
GET /t
--- error_code: 200
--- response_body
same store reused: 1
different store reused: 0
--- no_error_log
[error]
[warn]
--- skip_nginx
4: < 1.21.4