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 there are several
options to configure SSL:

  * `use_tls` is a boolean param to enable tls with tls_options provied below (`false` by default);
  * `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 8, 2024
1 parent 0e7af5a commit 596850c
Show file tree
Hide file tree
Showing 19 changed files with 1,487 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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,15 @@ 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:
* `use_tls` is a boolean param to enable tls with tls_options provied below (`false` by default);
* `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
133 changes: 131 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 = pcall(require, 'http.sslsocket')

local fio = require('fio')
local require = require
Expand All @@ -9,6 +10,7 @@ local mime_types = require('http.mime_types')
local codes = require('http.codes')

local log = require('log')
local ffi = require('ffi')
local socket = require('socket')
local json = require('json')
local errno = require 'errno'
Expand Down Expand Up @@ -1295,11 +1297,72 @@ local function url_for_httpd(httpd, name, args, query)
end
end

local function enable_tls(host, port, ssl_opts)
local ok, ctx = pcall(sslsocket.ctx, ffi.C.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.options.use_tls then
ssl_ctx = enable_tls(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 +1371,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 +1384,56 @@ local function httpd_start(self)
return self
end

local function validate_ssl_opts(ssl_opts)
if ssl_opts.ssl_cert_file ~= nil then
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
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
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
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
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
if type(ssl_opts.ssl_ciphers) ~= 'string' then
error("ssl_ciphers option must be a string")
end
end
end

local exports = {
_VERSION = require('http.version'),
DETACHED = DETACHED,
Expand All @@ -1340,6 +1453,20 @@ 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

if options.use_tls == true then
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,
})
end

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

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

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

0 comments on commit 596850c

Please sign in to comment.