Skip to content

Commit

Permalink
api: add SSL support
Browse files Browse the repository at this point in the history
It wasn't SSL support. After the patch it was added several options to
configure SSL, use one of them to enable it:

  * `ssl_cert_file` is a path to the SSL cert file;
  * `ssl_key_file` is a path to the SSL key file;
  * `ssl_ca_file` is a path to the SSL CA file;
  * `ssl_ciphers` is a colon-separated list of SSL ciphers;
  * `ssl_password` is a password for decrypting SSL private key;
  * `ssl_password_file` is a SSL file with key for decrypting SSL private key.

Closes #35
  • Loading branch information
themilchenko committed Nov 11, 2024
1 parent 0e7af5a commit 44a1a2e
Show file tree
Hide file tree
Showing 19 changed files with 1,375 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added

- SSL support (#199).

### Changed

### Fixed
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ httpd = require('http.server').new(host, port[, { options } ])
* `idle_timeout` - maximum amount of time an idle (keep-alive) connection will
remain idle before closing. When the idle timeout is exceeded, HTTP server
closes the keepalive connection. Default value: 0 seconds (disabled).
* `socket_timeout` - host resolving timeout in seconds (default 60).
* TLS options (to enable it, provide at least one option provided below):
* `ssl_cert_file` is a path to the SSL cert file;
* `ssl_key_file` is a path to the SSL key file;
* `ssl_ca_file` is a path to the SSL CA file;
* `ssl_ciphers` is a colon-separated list of SSL ciphers;
* `ssl_password` is a password for decrypting SSL private key;
* `ssl_password_file` is a SSL file with key for decrypting SSL private key.
## Using routes
Expand Down
152 changes: 150 additions & 2 deletions http/server.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
-- http.server

local lib = require('http.lib')
local sslsocket_supported, sslsocket = pcall(require, 'http.sslsocket')

local fio = require('fio')
local require = require
Expand Down Expand Up @@ -1295,11 +1296,76 @@ local function url_for_httpd(httpd, name, args, query)
end
end

local function create_ssl_ctx(host, port, ssl_opts)
if sslsocket_supported == false then
return nil, error("ssl socket is not supported")
end

local ok, ctx = pcall(sslsocket.ctx, sslsocket.tls_server_method())
if ok ~= true then
return nil, error(ctx)
end

local rc = sslsocket.ctx_use_private_key_file(ctx, ssl_opts.ssl_key_file,
ssl_opts.ssl_password, ssl_opts.ssl_password_file)
if rc == false then
errorf(
"Can't start server on %s:%s: %s %s",
host, port, 'Private key is invalid or password mismatch', ssl_opts.ssl_key_file
)
end

rc = sslsocket.ctx_use_certificate_file(ctx, ssl_opts.ssl_cert_file)
if rc == false then
errorf(
"Can't start server on %s:%s: %s %s",
host, port, 'Certificate is invalid', ssl_opts.ssl_cert_file
)
end

if ssl_opts.ssl_ca_file ~= nil then
rc = sslsocket.ctx_load_verify_locations(ctx, ssl_opts.ssl_ca_file)
if rc == false then
errorf(
"Can't start server on %s:%s: %s",
host, port, 'CA file is invalid'
)
end

sslsocket.ctx_set_verify(ctx, 0x01 + 0x02)
end

if ssl_opts.ssl_ciphers ~= nil then
rc = sslsocket.ctx_set_cipher_list(ctx, ssl_opts.ssl_ciphers)
if rc == false then
errorf(
"Can't start server on %s:%s: %s",
host, port, 'Ciphers is invalid'
)
end
end

return ctx
end

local function httpd_start(self)
if type(self) ~= 'table' then
error("httpd: usage: httpd:start()")
end

local ssl_ctx
if self.use_tls then
ssl_ctx = create_ssl_ctx(self.host, self.port, {
ssl_cert_file = self.options.ssl_cert_file,
ssl_key_file = self.options.ssl_key_file,
ssl_password = self.options.ssl_password,
ssl_password_file = self.options.ssl_password_file,
ssl_ca_file = self.options.ssl_ca_file,
ssl_ciphers = self.options.ssl_ciphers,
})
self.tcp_server_f = sslsocket.tcp_server
end

local server = self.tcp_server_f(self.host, self.port, {
name = 'http',
handler = function(...)
Expand All @@ -1308,7 +1374,7 @@ local function httpd_start(self)
self.internal.postprocess_client_handler()
end,
http_server = self,
})
}, self.options.socket_timeout, ssl_ctx)

if server == nil then
error(sprintf("Can't create tcp_server: %s", errno.strerror()))
Expand All @@ -1321,6 +1387,74 @@ local function httpd_start(self)
return self
end

-- validate_ssl_opts validates for ssl_opts and return true if at least one
-- ssl parameter is not nil.
local function validate_ssl_opts(ssl_opts)
local is_tls_enabled = false

if ssl_opts.ssl_cert_file ~= nil then
is_tls_enabled = true

if type(ssl_opts.ssl_cert_file) ~= 'string' then
error("ssl_cert_file option must be a string")
end
if fio.path.exists(ssl_opts.ssl_cert_file) ~= true then
errorf("file %q not exists", ssl_opts.ssl_cert_file)
end
end

if ssl_opts.ssl_key_file ~= nil then
is_tls_enabled = true

if type(ssl_opts.ssl_key_file) ~= 'string' then
error("ssl_key_file option must be a string")
end
if fio.path.exists(ssl_opts.ssl_key_file) ~= true then
errorf("file %q not exists", ssl_opts.ssl_key_file)
end
end

if ssl_opts.ssl_password ~= nil then
is_tls_enabled = true

if type(ssl_opts.ssl_password) ~= 'string' then
error("ssl_password option must be a string")
end
end

if ssl_opts.ssl_password_file then
is_tls_enabled = true

if type(ssl_opts.ssl_password_file) ~= 'string' then
error("ssl_password_file option must be a string")
end
if fio.path.exists(ssl_opts.ssl_password_file) ~= true then
errorf("file %q not exists", ssl_opts.ssl_password_file)
end
end

if ssl_opts.ssl_ca_file ~= nil then
is_tls_enabled = true

if type(ssl_opts.ssl_ca_file) ~= 'string' then
error("ssl_ca_file option must be a string")
end
if fio.path.exists(ssl_opts.ssl_ca_file) ~= true then
errorf("file %q not exists", ssl_opts.ssl_ca_file)
end
end

if ssl_opts.ssl_ciphers ~= nil then
is_tls_enabled = true

if type(ssl_opts.ssl_ciphers) ~= 'string' then
error("ssl_ciphers option must be a string")
end
end

return is_tls_enabled
end

local exports = {
_VERSION = require('http.version'),
DETACHED = DETACHED,
Expand All @@ -1340,6 +1474,18 @@ local exports = {
type(options.idle_timeout) ~= 'number' then
error('Option idle_timeout must be a number.')
end
if options.socket_timeout ~= nil and type(options.socket_timeout) ~= 'number' then
error('Option socket_timeout must be a number')
end

local is_tls_enabled = validate_ssl_opts({
ssl_cert_file = options.ssl_cert_file,
ssl_key_file = options.ssl_key_file,
ssl_password = options.ssl_password,
ssl_password_file = options.ssl_password_file,
ssl_ca_file = options.ssl_ca_file,
ssl_ciphers = options.ssl_ciphers,
})

local default = {
max_header_size = 4096,
Expand All @@ -1355,6 +1501,7 @@ local exports = {
display_errors = false,
disable_keepalive = {},
idle_timeout = 0, -- no timeout, option is disabled
socket_timeout = 60,
}

local self = {
Expand All @@ -1363,7 +1510,8 @@ local exports = {
is_run = false,
stop = httpd_stop,
start = httpd_start,
options = extend(default, options, true),
use_tls = is_tls_enabled,
options = extend(default, options, false),

routes = { },
iroutes = { },
Expand Down
Loading

0 comments on commit 44a1a2e

Please sign in to comment.