diff --git a/.travis.yml b/.travis.yml index e21d1f2fa1cf..318d688e47c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ addons: env: global: - - OPENRESTY_PREFIX=/usr/local/openresty + - OPENRESTY_PREFIX=/usr/local/openresty-debug before_install: - sudo cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1) @@ -30,7 +30,7 @@ install: - sudo apt-get -y install software-properties-common - sudo add-apt-repository -y "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main" - sudo apt-get update - - sudo apt-get install openresty + - sudo apt-get install openresty-debug - sudo luarocks install apisix-*.rockspec --only-deps - git clone https://github.com/openresty/test-nginx.git test-nginx diff --git a/Makefile b/Makefile index b522a0794f39..347aa22a8f9f 100644 --- a/Makefile +++ b/Makefile @@ -65,9 +65,10 @@ reload: .PHONY: install install: $(INSTALL) -d /usr/local/apisix/logs/ - $(INSTALL) -d /usr/local/apisix/conf/ + $(INSTALL) -d /usr/local/apisix/conf/cert $(INSTALL) conf/mime.types /usr/local/apisix/conf/mime.types $(INSTALL) conf/config.yaml /usr/local/apisix/conf/config.yaml + $(INSTALL) conf/cert/apisix.* /usr/local/apisix/conf/cert/ $(INSTALL) -d $(INST_LUADIR)/apisix/lua/apisix/core $(INSTALL) lua/*.lua $(INST_LUADIR)/apisix/lua/ diff --git a/apisix-0.4-3.rockspec b/apisix-0.4-4.rockspec similarity index 96% rename from apisix-0.4-3.rockspec rename to apisix-0.4-4.rockspec index 33aa020251c8..6aa76d9922f1 100644 --- a/apisix-0.4-3.rockspec +++ b/apisix-0.4-4.rockspec @@ -1,5 +1,5 @@ package = "apisix" -version = "0.4-3" +version = "0.4-4" supported_platforms = {"linux", "macosx"} source = { @@ -15,7 +15,7 @@ description = { } dependencies = { - "lua-resty-libr3 = 0.5", + "lua-resty-libr3 = 0.6", "lua-resty-template = 1.9-1", "lua-resty-etcd = 0.5", "lua-resty-balancer = 0.02rc5", diff --git a/bin/apisix b/bin/apisix index dfb08c894163..1ec6cee03a50 100755 --- a/bin/apisix +++ b/bin/apisix @@ -119,6 +119,10 @@ http { server { listen {* node_listen *}; + listen 9443 ssl; + ssl_certificate cert/apisix.crt; + ssl_certificate_key cert/apisix.key; + ssl_session_cache shared:SSL:1m; include mime.types; @@ -271,7 +275,8 @@ local function init_etcd(show_output) local uri = etcd_conf.host .. "/v2/keys" .. (etcd_conf.prefix or "") for _, dir_name in ipairs({"/routes", "/upstreams", "/services", - "/plugins", "/consumers", "/node_status"}) do + "/plugins", "/consumers", "/node_status", + "/ssl"}) do local cmd = "curl " .. uri .. dir_name .. "?prev_exist=false -X PUT -d dir=true 2>&1" local res = exec(cmd) diff --git a/conf/cert/apisix.crt b/conf/cert/apisix.crt new file mode 100644 index 000000000000..503f27797976 --- /dev/null +++ b/conf/cert/apisix.crt @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV +BAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G +A1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAgFw0xOTA2MjQyMjE4MDVa +GA8yMTE5MDUzMTIyMTgwNVowVjELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5n +RG9uZzEPMA0GA1UEBwwGWmh1SGFpMQ8wDQYDVQQKDAZpcmVzdHkxETAPBgNVBAMM +CHRlc3QuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyCM0rqJe +cvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5jhZB3W6BkWUWR4oNFLLSqcVb +VDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfoeLj0efMiOepOSZflj9Ob4yKR +2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5smPtW1Oc/BV5terhscJdOgmRr +abf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt6iMWEGeQU6mwPENgvj1olji2 +WjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiyVt1TmtMWn1ztk6FfLRqwJWR/ +Evm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1onpRVeXhrBajbCRDRBMwaNw/1 +/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2fzaqpIfyUbPST4GdqNG9NyIh +/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI1cGrGwyXbrieNp63AgMBAAGj +cTBvMB0GA1UdDgQWBBSZtSvV8mBwl0bpkvFtgyiOUUcbszAfBgNVHSMEGDAWgBSZ +tSvV8mBwl0bpkvFtgyiOUUcbszAMBgNVHRMEBTADAQH/MB8GA1UdEQQYMBaCCHRl +c3QuY29tggoqLnRlc3QuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQAHGEul/x7ViVgC +tC8CbXEslYEkj1XVr2Y4hXZXAXKd3W7V3TC8rqWWBbr6L/tsSVFt126V5WyRmOaY +1A5pju8VhnkhYxYfZALQxJN2tZPFVeME9iGJ9BE1wPtpMgITX8Rt9kbNlENfAgOl +PYzrUZN1YUQjX+X8t8/1VkSmyZysr6ngJ46/M8F16gfYXc9zFj846Z9VST0zCKob +rJs3GtHOkS9zGGldqKKCj+Awl0jvTstI4qtS1ED92tcnJh5j/SSXCAB5FgnpKZWy +hme45nBQj86rJ8FhN+/aQ9H9/2Ib6Q4wbpaIvf4lQdLUEcWAeZGW6Rk0JURwEog1 +7/mMgkapDglgeFx9f/XztSTrkHTaX4Obr+nYrZ2V4KOB4llZnK5GeNjDrOOJDk2y +IJFgBOZJWyS93dQfuKEj42hA79MuX64lMSCVQSjX+ipR289GQZqFrIhiJxLyA+Ve +U/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM= +-----END CERTIFICATE----- diff --git a/conf/cert/apisix.key b/conf/cert/apisix.key new file mode 100644 index 000000000000..71050679e422 --- /dev/null +++ b/conf/cert/apisix.key @@ -0,0 +1,39 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIG5AIBAAKCAYEAyCM0rqJecvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5 +jhZB3W6BkWUWR4oNFLLSqcVbVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfo +eLj0efMiOepOSZflj9Ob4yKR2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5s +mPtW1Oc/BV5terhscJdOgmRrabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt +6iMWEGeQU6mwPENgvj1olji2WjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiy +Vt1TmtMWn1ztk6FfLRqwJWR/Evm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1o +npRVeXhrBajbCRDRBMwaNw/1/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2 +fzaqpIfyUbPST4GdqNG9NyIh/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI +1cGrGwyXbrieNp63AgMBAAECggGBAJM8g0duoHmIYoAJzbmKe4ew0C5fZtFUQNmu +O2xJITUiLT3ga4LCkRYsdBnY+nkK8PCnViAb10KtIT+bKipoLsNWI9Xcq4Cg4G3t +11XQMgPPgxYXA6m8t+73ldhxrcKqgvI6xVZmWlKDPn+CY/Wqj5PA476B5wEmYbNC +GIcd1FLl3E9Qm4g4b/sVXOHARF6iSvTR+6ol4nfWKlaXSlx2gNkHuG8RVpyDsp9c +z9zUqAdZ3QyFQhKcWWEcL6u9DLBpB/gUjyB3qWhDMe7jcCBZR1ALyRyEjmDwZzv2 +jlv8qlLFfn9R29UI0pbuL1eRAz97scFOFme1s9oSU9a12YHfEd2wJOM9bqiKju8y +DZzePhEYuTZ8qxwiPJGy7XvRYTGHAs8+iDlG4vVpA0qD++1FTpv06cg/fOdnwshE +OJlEC0ozMvnM2rZ2oYejdG3aAnUHmSNa5tkJwXnmj/EMw1TEXf+H6+xknAkw05nh +zsxXrbuFUe7VRfgB5ElMA/V4NsScgQKBwQDmMRtnS32UZjw4A8DsHOKFzugfWzJ8 +Gc+3sTgs+4dNIAvo0sjibQ3xl01h0BB2Pr1KtkgBYB8LJW/FuYdCRS/KlXH7PHgX +84gYWImhNhcNOL3coO8NXvd6+m+a/Z7xghbQtaraui6cDWPiCNd/sdLMZQ/7LopM +RbM32nrgBKMOJpMok1Z6zsPzT83SjkcSxjVzgULNYEp03uf1PWmHuvjO1yELwX9/ +goACViF+jst12RUEiEQIYwr4y637GQBy+9cCgcEA3pN9W5OjSPDVsTcVERig8++O +BFURiUa7nXRHzKp2wT6jlMVcu8Pb2fjclxRyaMGYKZBRuXDlc/RNO3uTytGYNdC2 +IptU5N4M7iZHXj190xtDxRnYQWWo/PR6EcJj3f/tc3Itm1rX0JfuI3JzJQgDb9Z2 +s/9/ub8RRvmQV9LM/utgyOwNdf5dyVoPcTY2739X4ZzXNH+CybfNa+LWpiJIVEs2 +txXbgZrhmlaWzwA525nZ0UlKdfktdcXeqke9eBghAoHARVTHFy6CjV7ZhlmDEtqE +U58FBOS36O7xRDdpXwsHLnCXhbFu9du41mom0W4UdzjgVI9gUqG71+SXrKr7lTc3 +dMHcSbplxXkBJawND/Q1rzLG5JvIRHO1AGJLmRgIdl8jNgtxgV2QSkoyKlNVbM2H +Wy6ZSKM03lIj74+rcKuU3N87dX4jDuwV0sPXjzJxL7NpR/fHwgndgyPcI14y2cGz +zMC44EyQdTw+B/YfMnoZx83xaaMNMqV6GYNnTHi0TO2TAoHBAKmdrh9WkE2qsr59 +IoHHygh7Wzez+Ewr6hfgoEK4+QzlBlX+XV/9rxIaE0jS3Sk1txadk5oFDebimuSk +lQkv1pXUOqh+xSAwk5v88dBAfh2dnnSa8HFN3oz+ZfQYtnBcc4DR1y2X+fVNgr3i +nxruU2gsAIPFRnmvwKPc1YIH9A6kIzqaoNt1f9VM243D6fNzkO4uztWEApBkkJgR +4s/yOjp6ovS9JG1NMXWjXQPcwTq3sQVLnAHxZRJmOvx69UmK4QKBwFYXXjeXiU3d +bcrPfe6qNGjfzK+BkhWznuFUMbuxyZWDYQD5yb6ukUosrj7pmZv3BxKcKCvmONU+ +CHgIXB+hG+R9S2mCcH1qBQoP/RSm+TUzS/Bl2UeuhnFZh2jSZQy3OwryUi6nhF0u +LDzMI/6aO1ggsI23Ri0Y9ZtqVKczTkxzdQKR9xvoNBUufjimRlS80sJCEB3Qm20S +wzarryret/7GFW1/3cz+hTj9/d45i25zArr3Pocfpur5mfz3fJO8jg== +-----END RSA PRIVATE KEY----- diff --git a/conf/cert/openssl.conf b/conf/cert/openssl.conf new file mode 100644 index 000000000000..5a0257986d5d --- /dev/null +++ b/conf/cert/openssl.conf @@ -0,0 +1,24 @@ +[req] +distinguished_name = req_distinguished_name +x509_extensions = v3_req +prompt = no + +[req_distinguished_name] +C = CN +ST = GuangDong +L = ZhuHai +O = iresty +CN = test.com + +[v3_req] +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer +basicConstraints = CA:TRUE +subjectAltName = @alt_names + +[alt_names] +DNS.1 = test.com +DNS.2 = *.test.com + +## openssl genrsa -out apisix.key 3072 -nodes +## openssl req -new -x509 -key apisix.key -sha256 -config openssl.conf -out apisix.crt -days 36500 diff --git a/conf/nginx.conf b/conf/nginx.conf index 5aa29a7b8875..32e07de8ce16 100644 --- a/conf/nginx.conf +++ b/conf/nginx.conf @@ -66,6 +66,11 @@ http { } server { + listen 9443 ssl; + ssl_certificate cert/apisix.crt; + ssl_certificate_key cert/apisix.key; + ssl_session_cache shared:SSL:1m; + listen 9080; include mime.types; @@ -83,6 +88,10 @@ http { } } + ssl_certificate_by_lua_block { + apisix.ssl_phase() + } + location / { set $upstream_scheme 'http'; set $upstream_host $host; diff --git a/lua/apisix.lua b/lua/apisix.lua index e1b17ddd3537..985c5d006386 100644 --- a/lua/apisix.lua +++ b/lua/apisix.lua @@ -6,11 +6,13 @@ local router = require("apisix.route").get local plugin = require("apisix.plugin") local load_balancer = require("apisix.balancer").run local service_fetch = require("apisix.service").get +local ssl_match = require("apisix.ssl").match local admin_init = require("apisix.admin.init") local get_var = require("resty.ngxvar").fetch local ngx = ngx local get_method = ngx.req.get_method local ngx_exit = ngx.exit +local ngx_ERROR = ngx.ERROR local math = math local match_opts = {} @@ -49,6 +51,7 @@ function _M.init_worker() require("apisix.consumer").init_worker() require("apisix.heartbeat").init_worker() require("apisix.admin.init").init_worker() + require("apisix.ssl").init_worker() end @@ -87,24 +90,40 @@ local function run_plugin(phase, plugins, api_ctx) end +function _M.ssl_phase() + local ngx_ctx = ngx.ctx + local api_ctx = ngx_ctx.api_ctx + + if api_ctx == nil then + api_ctx = core.tablepool.fetch("api_ctx", 0, 32) + ngx_ctx.api_ctx = api_ctx + end + + local ok, err = ssl_match(api_ctx) + if not ok then + if err then + core.log.error("failed to fetch ssl config: ", err) + end + return ngx_exit(ngx_ERROR) + end +end + + function _M.access_phase() local ngx_ctx = ngx.ctx local api_ctx = ngx_ctx.api_ctx if api_ctx == nil then api_ctx = core.tablepool.fetch("api_ctx", 0, 32) + ngx_ctx.api_ctx = api_ctx end core.ctx.set_vars_meta(api_ctx) - ngx_ctx.api_ctx = api_ctx - core.table.clear(match_opts) match_opts.method = api_ctx.var.method match_opts.host = api_ctx.var.host - api_ctx.uri_parse_param = core.tablepool.fetch("uri_parse_param", 0, 4) - local ok = router():dispatch2(api_ctx.uri_parse_param, - api_ctx.var.uri, match_opts, api_ctx) + local ok = router():dispatch2(nil, api_ctx.var.uri, match_opts, api_ctx) if not ok then core.log.info("not find any matched route") return core.response.exit(404) @@ -165,11 +184,15 @@ end function _M.log_phase() local api_ctx = run_plugin("log") if api_ctx then - core.tablepool.release("uri_parse_param", api_ctx.uri_parse_param) + if api_ctx.uri_parse_param then + core.tablepool.release("uri_parse_param", api_ctx.uri_parse_param) + end + core.ctx.release_vars(api_ctx) if api_ctx.plugins then core.tablepool.release("plugins", api_ctx.plugins) end + core.tablepool.release("api_ctx", api_ctx) end end diff --git a/lua/apisix/admin/init.lua b/lua/apisix/admin/init.lua index c78d6ae4641b..db5e10f9ca7d 100644 --- a/lua/apisix/admin/init.lua +++ b/lua/apisix/admin/init.lua @@ -6,11 +6,12 @@ local ngx = ngx local resources = { - routes = require("apisix.admin.routes"), - services = require("apisix.admin.services"), + routes = require("apisix.admin.routes"), + services = require("apisix.admin.services"), upstreams = require("apisix.admin.upstreams"), consumers = require("apisix.admin.consumers"), - schema = require("apisix.admin.schema"), + schema = require("apisix.admin.schema"), + ssl = require("apisix.admin.ssl"), } @@ -52,17 +53,17 @@ end local uri_route = { { - uri = [[/apisix/admin/{res:routes|services|upstreams|consumers}]], + path = [[/apisix/admin/{res:routes|services|upstreams|consumers|ssl}]], handler = run }, { - uri = [[/apisix/admin/{res:routes|services|upstreams|consumers}]] + path = [[/apisix/admin/{res:routes|services|upstreams|consumers|ssl}]] .. [[/{id:[\d\w_]+}]], handler = run }, { - uri = [[/apisix/admin/{res:schema}/]] - .. [[{id:route|service|upstream|consumer}]], + path = [[/apisix/admin/{res:schema}/]] + .. [[{id:route|service|upstream|consumer|ssl}]], handler = run }, } diff --git a/lua/apisix/admin/ssl.lua b/lua/apisix/admin/ssl.lua new file mode 100644 index 000000000000..4eab8190a0c0 --- /dev/null +++ b/lua/apisix/admin/ssl.lua @@ -0,0 +1,149 @@ +local core = require("apisix.core") +local schema_plugin = require("apisix.admin.plugins").check_schema +local tostring = tostring + + +local _M = { + version = 0.1, +} + + +local function check_conf(id, conf, need_id) + if not conf then + return nil, {error_msg = "missing configurations"} + end + + id = id or conf.id + if need_id and not id then + return nil, {error_msg = "missing ssl id"} + end + + if not need_id and id then + return nil, {error_msg = "wrong ssl id, do not need it"} + end + + if need_id and conf.id and tostring(conf.id) ~= tostring(id) then + return nil, {error_msg = "wrong ssl id"} + end + + core.log.info("schema: ", core.json.delay_encode(core.schema.ssl)) + core.log.info("conf : ", core.json.delay_encode(conf)) + local ok, err = core.schema.check(core.schema.ssl, conf) + if not ok then + return nil, {error_msg = "invalid configuration: " .. err} + end + + local upstream_id = conf.upstream_id + if upstream_id then + local key = "/upstreams/" .. upstream_id + local res, err = core.etcd.get(key) + if not res then + return nil, {error_msg = "failed to fetch upstream info by " + .. "upstream id [" .. upstream_id .. "]: " + .. err} + end + + if res.status ~= 200 then + return nil, {error_msg = "failed to fetch upstream info by " + .. "upstream id [" .. upstream_id .. "], " + .. "response code: " .. res.status} + end + end + + local service_id = conf.service_id + if service_id then + local key = "/services/" .. service_id + local res, err = core.etcd.get(key) + if not res then + return nil, {error_msg = "failed to fetch service info by " + .. "service id [" .. service_id .. "]: " + .. err} + end + + if res.status ~= 200 then + return nil, {error_msg = "failed to fetch service info by " + .. "service id [" .. service_id .. "], " + .. "response code: " .. res.status} + end + end + + if conf.plugins then + local ok, err = schema_plugin(conf.plugins) + if not ok then + return nil, {error_msg = err} + end + end + + return need_id and id or true +end + + +function _M.put(id, conf) + local id, err = check_conf(id, conf, true) + if not id then + return 400, err + end + + local key = "/ssl/" .. id + local res, err = core.etcd.set(key, conf) + if not res then + core.log.error("failed to put ssl[", key, "]: ", err) + return 500, {error_msg = err} + end + + return res.status, res.body +end + + +function _M.get(id) + local key = "/ssl" + if id then + key = key .. "/" .. id + end + + local res, err = core.etcd.get(key) + if not res then + core.log.error("failed to get ssl[", key, "]: ", err) + return 500, {error_msg = err} + end + + return res.status, res.body +end + + +function _M.post(id, conf) + local id, err = check_conf(id, conf, false) + if not id then + return 400, err + end + + local key = "/ssl" + -- core.log.info("key: ", key) + local res, err = core.etcd.push("/ssl", conf) + if not res then + core.log.error("failed to post ssl[", key, "]: ", err) + return 500, {error_msg = err} + end + + return res.status, res.body +end + + +function _M.delete(id) + if not id then + return 400, {error_msg = "missing ssl id"} + end + + local key = "/ssl/" .. id + -- core.log.info("key: ", key) + local res, err = core.etcd.delete(key) + if not res then + core.log.error("failed to delete ssl[", key, "]: ", err) + return 500, {error_msg = err} + end + + return res.status, res.body +end + + +return _M diff --git a/lua/apisix/core/config_etcd.lua b/lua/apisix/core/config_etcd.lua index ebf45a714dbc..fa098097dceb 100644 --- a/lua/apisix/core/config_etcd.lua +++ b/lua/apisix/core/config_etcd.lua @@ -157,8 +157,8 @@ local function sync_data(self) end local res, err = waitdir(self.etcd_cli, self.key, self.prev_index + 1) - log.info("waitdir key: ", self.key, " prev_index: ", self.prev_index + 1, - " res: ", json.delay_encode(res)) + log.debug("waitdir key: ", self.key, " prev_index: ", self.prev_index + 1, + " res: ", json.delay_encode(res)) if not res then return false, err end diff --git a/lua/apisix/core/schema.lua b/lua/apisix/core/schema.lua index 5fa1a0ff8089..2c3947af91dc 100644 --- a/lua/apisix/core/schema.lua +++ b/lua/apisix/core/schema.lua @@ -7,7 +7,7 @@ local json_doc = json.Document local cached_sd = require("apisix.core.lrucache").new({count = 1000, ttl = 0}) -local _M = {version = 0.1} +local _M = {version = 0.2} local function create_validator(schema) @@ -215,4 +215,23 @@ _M.consumer = { _M.upstream = upstream_schema +_M.ssl = { + type = "object", + properties = { + cert = { + type = "string", minLength = 128, maxLength = 4096 + }, + key = { + type = "string", minLength = 128, maxLength = 4096 + }, + sni = { + type = "string", + pattern = [[^\*?[0-9a-zA-Z-.]+$]], + } + }, + required = {"sni", "key", "cert"}, + additionalProperties = false, +} + + return _M diff --git a/lua/apisix/route.lua b/lua/apisix/route.lua index 643486917277..926cbf4f4930 100644 --- a/lua/apisix/route.lua +++ b/lua/apisix/route.lua @@ -13,25 +13,30 @@ local _M = {version = 0.1} local empty_tab = {} + local route_items local function create_r3_router(routes) routes = routes or empty_tab local api_routes = plugin.api_routes() - local items = core.table.new(#api_routes + #routes, 0) + route_items = core.table.new(#api_routes + #routes, 0) local idx = 0 for _, route in ipairs(api_routes) do if type(route) == "table" then idx = idx + 1 - items[idx] = route + route_items[idx] = { + path = route.uri, + handler = route.handler, + method = route.methods, + } end end for _, route in ipairs(routes) do if type(route) == "table" then idx = idx + 1 - items[idx] = { - uri = route.value.uri, + route_items[idx] = { + path = route.value.uri, method = route.value.methods, host = route.value.host, handler = function (params, api_ctx) @@ -42,7 +47,10 @@ local function create_r3_router(routes) end end - return r3router.new(items) + core.log.info("route items: ", core.json.delay_encode(route_items, true)) + local r3 = r3router.new(route_items) + r3:compile() + return r3 end diff --git a/lua/apisix/ssl.lua b/lua/apisix/ssl.lua new file mode 100644 index 000000000000..a1455c33f998 --- /dev/null +++ b/lua/apisix/ssl.lua @@ -0,0 +1,145 @@ +-- Copyright (C) Yuansheng Wang + +local r3router = require("resty.r3") +local core = require("apisix.core") +local ngx_ssl = require("ngx.ssl") +local ffi = require("ffi") +local get_request = require("resty.core.base").get_request +local errmsg = ffi.new("char *[1]") +local C = ffi.C +local ipairs = ipairs +local type = type +local error = error +local ffi_string = ffi.string +local ssl + + +ffi.cdef[[ +int ngx_http_lua_ffi_cert_pem_to_der(const unsigned char *pem, + size_t pem_len, unsigned char *der, char **err); +int ngx_http_lua_ffi_ssl_set_der_certificate(void *r, + const char *data, size_t len, char **err); +]] + + +local _M = { + version = 0.1, + server_name = ngx_ssl.server_name, +} + + + local empty_tab = {} + local route_items +local function create_r3_router(ssl_items) + local ssl_items = ssl_items or empty_tab + + route_items = core.table.new(#ssl_items, 0) + local idx = 0 + + for _, ssl in ipairs(ssl_items) do + if type(ssl) == "table" then + local sni = ssl.value.sni:reverse() + if sni:sub(#sni) == "*" then + sni = sni:sub(1, #sni - 1) .. "{prefix:.+}" + end + + idx = idx + 1 + route_items[idx] = { + path = sni, + handler = function (params, api_ctx) + api_ctx.matched_ssl = ssl + end + } + end + end + + core.log.info("route items: ", core.json.delay_encode(route_items, true)) + local r3 = r3router.new(route_items) + r3:compile() + return r3 +end + + +local function set_pem_ssl_key(cert, pkey) + local r = get_request() + if r == nil then + return false, "no request found" + end + + local out = ffi.new("char [?]", #cert) + local rc = C.ngx_http_lua_ffi_cert_pem_to_der(cert, #cert, out, errmsg) + if rc < 1 then + return false, "failed to parse PEM cert: " .. ffi_string(errmsg[0]) + end + + local cert_der = ffi_string(out, rc) + local rc = C.ngx_http_lua_ffi_ssl_set_der_certificate(r, cert_der, + #cert_der, errmsg) + if rc ~= 0 then + return false, "failed to set DER cert: " .. ffi_string(errmsg[0]) + end + + out = ffi.new("char [?]", #pkey) + local rc = C.ngx_http_lua_ffi_priv_key_pem_to_der(pkey, #pkey, out, errmsg) + if rc < 1 then + return false, "failed to parse PEM priv key: " .. ffi_string(errmsg[0]) + end + + local pkey_der = ffi_string(out, rc) + + local rc = C.ngx_http_lua_ffi_ssl_set_der_private_key(r, pkey_der, + #pkey_der, errmsg) + if rc ~= 0 then + return false, "failed to set DER priv key: " .. ffi_string(errmsg[0]) + end + + return true +end + + +function _M.match(api_ctx) + ngx_ssl.clear_certs() + + local r3, err = core.lrucache.global("/ssl", ssl.conf_version, + create_r3_router, ssl.values) + if not r3 then + return false, "gailed to fetch ssl router: " .. err + end + + local sni = ngx_ssl.server_name() + if type(sni) ~= "string" then + return false, "gailed to fetch SNI: " .. err + end + + core.log.debug("sni: ", sni) + local ok = r3:dispatch2(nil, sni:reverse(), nil, api_ctx) + if not ok then + core.log.warn("not found any valid sni configuration") + return false + end + + local matched_ssl = api_ctx.matched_ssl + core.log.info("debug: ", core.json.delay_encode(matched_ssl, true)) + ok, err = set_pem_ssl_key(matched_ssl.value.cert, matched_ssl.value.key) + if not ok then + return false, err + end + + return true +end + + +function _M.init_worker() + local err + ssl, err = core.config.new("/ssl", { + automatic = true, + item_schema = core.schema.ssl + }) + if not ssl then + error("failed to create etcd instance to fetch /ssl " + .. "automaticly: " .. err) + end +end + + +return _M diff --git a/t/APISix.pm b/t/APISix.pm index cec329908c77..789cd39b42e0 100644 --- a/t/APISix.pm +++ b/t/APISix.pm @@ -21,7 +21,10 @@ sub read_file($) { } my $yaml_config = read_file("conf/config.yaml"); +my $ssl_crt = read_file("conf/cert/apisix.crt"); +my $ssl_key = read_file("conf/cert/apisix.key"); $yaml_config =~ s/node_listen: 9080/node_listen: 1984/; +$yaml_config =~ s/enable_heartbeat: true/enable_heartbeat: false/; add_block_preprocessor(sub { @@ -70,10 +73,14 @@ _EOC_ listen 1981; listen 1982; + server_tokens off; + location / { content_by_lua_block { require("lib.server").go() } + + more_clear_headers Date; } } @@ -81,10 +88,22 @@ _EOC_ $block->set_value("http_config", $http_config); + my $TEST_NGINX_HTML_DIR = $ENV{TEST_NGINX_HTML_DIR} ||= html_dir(); + my $wait_etcd_sync = $block->wait_etcd_sync // 0.1; my $config = $block->config // ''; $config .= <<_EOC_; + listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl; + + ssl_certificate cert/apisix.crt; + ssl_certificate_key cert/apisix.key; + lua_ssl_trusted_certificate cert/apisix.crt; + + ssl_certificate_by_lua_block { + apisix.ssl_phase() + } + location = /apisix/nginx_status { allow 127.0.0.0/24; access_log off; @@ -137,6 +156,10 @@ _EOC_ $user_files .= <<_EOC_; >>> ../conf/config.yaml $user_yaml_config +>>> ../conf/cert/apisix.crt +$ssl_crt +>>> ../conf/cert/apisix.key +$ssl_key _EOC_ $block->set_value("user_files", $user_files); diff --git a/t/admin/schema.t b/t/admin/schema.t index 320a6b3fe1ac..633d87f0b4a3 100644 --- a/t/admin/schema.t +++ b/t/admin/schema.t @@ -24,7 +24,7 @@ qr/"plugins": \{"type":"object"}/ --- request GET /apisix/admin/schema/service --- response_body eval -qr/"upstream":\{"type":"object"/ +qr/"required":\["upstream"\]/ --- no_error_log [error] @@ -54,3 +54,43 @@ POST /apisix/admin/schema/service --- error_code: 404 --- no_error_log [error] + + + +=== TEST 6: ssl +--- config +location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/schema/ssl', + ngx.HTTP_GET, + nil, + { + type = "object", + properties = { + cert = { + type = "string", minLength = 128, maxLength = 4096 + }, + key = { + type = "string", minLength = 128, maxLength = 4096 + }, + sni = { + type = "string", + pattern = [[^\*?[0-9a-zA-Z-.]+$]], + } + }, + required = {"sni", "key", "cert"}, + additionalProperties = false, + } + ) + + ngx.status = code + ngx.say(body) + } +} +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] diff --git a/t/admin/ssl.t b/t/admin/ssl.t new file mode 100644 index 000000000000..6fe6cc44b783 --- /dev/null +++ b/t/admin/ssl.t @@ -0,0 +1,253 @@ +use t::APISix 'no_plan'; + +no_root_location(); + +run_tests; + +__DATA__ + +=== TEST 1: set ssl(id: 1) +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_cert = t.read_file("conf/cert/apisix.crt") + local ssl_key = t.read_file("conf/cert/apisix.key") + local data = {cert = ssl_cert, key = ssl_key, sni = "test.com"} + + local code, body = t.test('/apisix/admin/ssl/1', + ngx.HTTP_PUT, + core.json.encode(data), + [[{ + "node": { + "value": { + "sni": "test.com" + }, + "key": "/apisix/ssl/1" + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 2: get ssl(id: 1) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/ssl/1', + ngx.HTTP_GET, + nil, + [[{ + "node": { + "value": { + "sni": "test.com" + }, + "key": "/apisix/ssl/1" + }, + "action": "get" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 3: delete ssl(id: 1) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, message = t('/apisix/admin/ssl/1', + ngx.HTTP_DELETE, + nil, + [[{ + "action": "delete" + }]] + ) + ngx.say("[delete] code: ", code, " message: ", message) + } + } +--- request +GET /t +--- response_body +[delete] code: 200 message: passed +--- no_error_log +[error] + + + +=== TEST 4: delete ssl(id: 99999999999999) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code = t('/apisix/admin/ssl/99999999999999', + ngx.HTTP_DELETE, + nil, + [[{ + "action": "delete" + }]] + ) + ngx.say("[delete] code: ", code) + } + } +--- request +GET /t +--- response_body +[delete] code: 404 +--- no_error_log +[error] + + + +=== TEST 5: push ssl + delete +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_cert = t.read_file("conf/cert/apisix.crt") + local ssl_key = t.read_file("conf/cert/apisix.key") + local data = {cert = ssl_cert, key = ssl_key, sni = "foo.com"} + + local code, message, res = t.test('/apisix/admin/ssl', + ngx.HTTP_POST, + core.json.encode(data), + [[{ + "node": { + "value": { + "sni": "foo.com" + } + }, + "action": "create" + }]] + ) + + if code ~= 200 then + ngx.status = code + ngx.say(message) + return + end + + ngx.say("[push] code: ", code, " message: ", message) + + local id = string.sub(res.node.key, #"/apisix/ssl/" + 1) + code, message = t.test('/apisix/admin/ssl/' .. id, + ngx.HTTP_DELETE, + nil, + [[{ + "action": "delete" + }]] + ) + ngx.say("[delete] code: ", code, " message: ", message) + } + } +--- request +GET /t +--- response_body +[push] code: 200 message: passed +[delete] code: 200 message: passed +--- no_error_log +[error] + + + +=== TEST 6: missing certificate information +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_cert = t.read_file("conf/cert/apisix.crt") + local ssl_key = t.read_file("conf/cert/apisix.key") + local data = {sni = "foo.com"} + + local code, body = t.test('/apisix/admin/ssl/1', + ngx.HTTP_PUT, + core.json.encode(data), + [[{ + "node": { + "value": { + "sni": "foo.com" + }, + "key": "/apisix/ssl/1" + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body +{"error_msg":"invalid configuration: invalid \"required\" in docuement at pointer \"#\""} +--- no_error_log +[error] + + + +=== TEST 7: wildcard host name +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_cert = t.read_file("conf/cert/apisix.crt") + local ssl_key = t.read_file("conf/cert/apisix.key") + local data = {cert = ssl_cert, key = ssl_key, sni = "*.foo.com"} + + local code, body = t.test('/apisix/admin/ssl/1', + ngx.HTTP_PUT, + core.json.encode(data), + [[{ + "node": { + "value": { + "sni": "*.foo.com" + }, + "key": "/apisix/ssl/1" + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] diff --git a/t/lib/test_admin.lua b/t/lib/test_admin.lua index 6fd7bf510946..fc2d0291c725 100644 --- a/t/lib/test_admin.lua +++ b/t/lib/test_admin.lua @@ -19,7 +19,8 @@ local function com_tab(pattern, data, deep) elseif v ~= data[k] then return false, "path: " .. table.concat(dir_names, "->", 1, deep) - .. " expect: " .. v .. " got: " .. data[k] + .. " expect: " .. tostring(v) .. " got: " + .. tostring(data[k]) end end @@ -39,7 +40,11 @@ function _M.test(uri, method, body, pattern) end local res_data = json.decode(res.body) - local ok, err = com_tab(json.decode(pattern), res_data) + if type(pattern) == "string" then + pattern = json.decode(pattern) + end + + local ok, err = com_tab(pattern, res_data) if not ok then return 500, "failed, " .. err, res_data end @@ -48,4 +53,12 @@ function _M.test(uri, method, body, pattern) end +function _M.read_file(path) + local f = assert(io.open(path, "rb")) + local cert = f:read("*all") + f:close() + return cert +end + + return _M diff --git a/t/node/ssl.t b/t/node/ssl.t new file mode 100644 index 000000000000..aec97eedccd5 --- /dev/null +++ b/t/node/ssl.t @@ -0,0 +1,412 @@ +use t::APISix 'no_plan'; + +log_level('debug'); +no_root_location(); + +$ENV{TEST_NGINX_HTML_DIR} ||= html_dir(); + +run_tests; + +__DATA__ + +=== TEST 1: set ssl(sni: www.test.com) +--- config +location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_cert = t.read_file("conf/cert/apisix.crt") + local ssl_key = t.read_file("conf/cert/apisix.key") + local data = {cert = ssl_cert, key = ssl_key, sni = "www.test.com"} + + local code, body = t.test('/apisix/admin/ssl/1', + ngx.HTTP_PUT, + core.json.encode(data), + [[{ + "node": { + "value": { + "sni": "www.test.com" + }, + "key": "/apisix/ssl/1" + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.say(body) + } +} +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 2: set route(id: 1) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 3: client request +--- config +location /t { + content_by_lua_block { + -- etcd sync + ngx.sleep(0.2) + + do + local sock = ngx.socket.tcp() + + sock:settimeout(2000) + + local ok, err = sock:connect("unix:$TEST_NGINX_HTML_DIR/nginx.sock") + if not ok then + ngx.say("failed to connect: ", err) + return + end + + ngx.say("connected: ", ok) + + local sess, err = sock:sslhandshake(nil, "www.test.com", true) + if not sess then + ngx.say("failed to do SSL handshake: ", err) + return + end + + ngx.say("ssl handshake: ", type(sess)) + + local req = "GET /hello HTTP/1.0\r\nHost: www.test.com\r\nConnection: close\r\n\r\n" + local bytes, err = sock:send(req) + if not bytes then + ngx.say("failed to send http request: ", err) + return + end + + ngx.say("sent http request: ", bytes, " bytes.") + + while true do + local line, err = sock:receive() + if not line then + -- ngx.say("failed to receive response status line: ", err) + break + end + + ngx.say("received: ", line) + end + + local ok, err = sock:close() + ngx.say("close: ", ok, " ", err) + end -- do + -- collectgarbage() + } +} +--- request +GET /t +--- response_body eval +qr{connected: 1 +ssl handshake: userdata +sent http request: 62 bytes. +received: HTTP/1.1 200 OK +received: Content-Type: text/plain +received: Connection: close +received: Server: openresty +received: \nreceived: hello world +close: 1 nil} +--- error_log +lua ssl server name: "www.test.com" +--- no_error_log +[error] +[alert] + + + +=== TEST 4: client request(no cert domain) +--- config +location /t { + content_by_lua_block { + -- etcd sync + ngx.sleep(0.2) + + do + local sock = ngx.socket.tcp() + + sock:settimeout(2000) + + local ok, err = sock:connect("unix:$TEST_NGINX_HTML_DIR/nginx.sock") + if not ok then + ngx.say("failed to connect: ", err) + return + end + + ngx.say("connected: ", ok) + + local sess, err = sock:sslhandshake(nil, "no-cert.com", true) + if not sess then + ngx.say("failed to do SSL handshake: ", err) + return + end + end + } +} +--- request +GET /t +--- response_body +connected: 1 +failed to do SSL handshake: handshake failed +--- error_log +SSL_do_handshake() failed (SSL: error: + + + +=== TEST 5: set ssl(sni: wildcard) +--- config +location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_cert = t.read_file("conf/cert/apisix.crt") + local ssl_key = t.read_file("conf/cert/apisix.key") + local data = {cert = ssl_cert, key = ssl_key, sni = "*.test.com"} + + local code, body = t.test('/apisix/admin/ssl/1', + ngx.HTTP_PUT, + core.json.encode(data), + [[{ + "node": { + "value": { + "sni": "*.test.com" + }, + "key": "/apisix/ssl/1" + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.say(body) + } +} +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 6: client request +--- config +location /t { + content_by_lua_block { + -- etcd sync + ngx.sleep(0.2) + + do + local sock = ngx.socket.tcp() + + sock:settimeout(2000) + + local ok, err = sock:connect("unix:$TEST_NGINX_HTML_DIR/nginx.sock") + if not ok then + ngx.say("failed to connect: ", err) + return + end + + ngx.say("connected: ", ok) + + local sess, err = sock:sslhandshake(nil, "www.test.com", false) + if not sess then + ngx.say("failed to do SSL handshake: ", err) + return + end + + ngx.say("ssl handshake: ", type(sess)) + + local req = "GET /hello HTTP/1.0\r\nHost: www.test.com\r\nConnection: close\r\n\r\n" + local bytes, err = sock:send(req) + if not bytes then + ngx.say("failed to send http request: ", err) + return + end + + ngx.say("sent http request: ", bytes, " bytes.") + + while true do + local line, err = sock:receive() + if not line then + -- ngx.say("failed to receive response status line: ", err) + break + end + + ngx.say("received: ", line) + end + + local ok, err = sock:close() + ngx.say("close: ", ok, " ", err) + end -- do + -- collectgarbage() + } +} +--- request +GET /t +--- response_body eval +qr{connected: 1 +ssl handshake: userdata +sent http request: 62 bytes. +received: HTTP/1.1 200 OK +received: Content-Type: text/plain +received: Connection: close +received: Server: openresty +received: \nreceived: hello world +close: 1 nil} +--- error_log +lua ssl server name: "www.test.com" +--- no_error_log +[error] +[alert] + + + +=== TEST 7: set ssl(sni: test.com) +--- config +location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_cert = t.read_file("conf/cert/apisix.crt") + local ssl_key = t.read_file("conf/cert/apisix.key") + local data = {cert = ssl_cert, key = ssl_key, sni = "test.com"} + + local code, body = t.test('/apisix/admin/ssl/1', + ngx.HTTP_PUT, + core.json.encode(data), + [[{ + "node": { + "value": { + "sni": "test.com" + }, + "key": "/apisix/ssl/1" + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.say(body) + } +} +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 8: client request +--- config +location /t { + content_by_lua_block { + -- etcd sync + ngx.sleep(0.2) + + do + local sock = ngx.socket.tcp() + + sock:settimeout(2000) + + local ok, err = sock:connect("unix:$TEST_NGINX_HTML_DIR/nginx.sock") + if not ok then + ngx.say("failed to connect: ", err) + return + end + + ngx.say("connected: ", ok) + + local sess, err = sock:sslhandshake(nil, "test.com", false) + if not sess then + ngx.say("failed to do SSL handshake: ", err) + return + end + + ngx.say("ssl handshake: ", type(sess)) + + local req = "GET /hello HTTP/1.0\r\nHost: test.com\r\nConnection: close\r\n\r\n" + local bytes, err = sock:send(req) + if not bytes then + ngx.say("failed to send http request: ", err) + return + end + + ngx.say("sent http request: ", bytes, " bytes.") + + while true do + local line, err = sock:receive() + if not line then + -- ngx.say("failed to receive response status line: ", err) + break + end + + ngx.say("received: ", line) + end + + local ok, err = sock:close() + ngx.say("close: ", ok, " ", err) + end -- do + -- collectgarbage() + } +} +--- request +GET /t +--- response_body eval +qr{connected: 1 +ssl handshake: userdata +sent http request: 58 bytes. +received: HTTP/1.1 200 OK +received: Content-Type: text/plain +received: Connection: close +received: Server: openresty +received: \nreceived: hello world +close: 1 nil} +--- error_log +lua ssl server name: "test.com" +--- no_error_log +[error] +[alert] diff --git a/t/plugin/prometheus.t b/t/plugin/prometheus.t index f0cc79e51926..2f048618c506 100644 --- a/t/plugin/prometheus.t +++ b/t/plugin/prometheus.t @@ -135,7 +135,7 @@ apisix_etcd_reachable 1 --- request GET /apisix/prometheus/metrics --- response_body eval -qr/apisix_bandwidth\{type="egress",service="localhost"\} 1278/ +qr/apisix_bandwidth\{type="egress",service="localhost"\} \d+/ --- no_error_log [error] @@ -270,6 +270,6 @@ passed --- request GET /apisix/prometheus/metrics --- response_body eval -qr/apisix_bandwidth\{type="egress",service="localhost"\} 1825/ +qr/apisix_bandwidth\{type="egress",service="localhost"\} \d+/ --- no_error_log [error]