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
253 changes: 164 additions & 89 deletions deps/secure-socket/biowrap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,125 +15,200 @@ See the License for the specific language governing permissions and
limitations under the License.

--]]
local openssl = require('openssl')
local openssl = require("openssl")
local uv = require("uv")

local BIO_BUFFER_SIZE = 8192
local PEEK_LENGTH = 1

local function closeSocket(socket)
if not socket:is_closing() then
socket:close()
end
end

-- writeCipher is called when ssl needs something written on the socket
-- handshakeComplete is called when the handhake is complete and it's safe
-- onPlain is called when plaintext comes out.
return function (ctx, isServer, socket, handshakeComplete, servername)
local function wrapSocketMethod(socket, method)
return function(_, ...)
return method(socket, ...)
end
end

local bin, bout = openssl.bio.mem(8192), openssl.bio.mem(8192)
local ssl = ctx:ssl(bin, bout, isServer)
-- Flush the bout buffer into the wrapped socket
-- i.e. send the encrypted data
local function flushSecureSocket(ssocket, callback)
local chunks = {}
local i = 0
while ssocket.bout:pending() > 0 do
i = i + 1
chunks[i] = ssocket.bout:read()
end
if i == 0 then
if callback then callback() end
return true
end
return ssocket.handle:write(chunks, callback)
end

if not isServer and servername then
ssl:set('hostname', servername)
local function readIncoming(ssocket)
if not ssocket.onPlain then
return
end
while true do
-- TODO: handle read errors and shutdowns
local plain = ssocket.ssl:read()
if not plain then break end
ssocket.onPlain(nil, plain)
end
end

local ssocket = {tls=true}
local onPlain
---@param socket uv_stream_t
---@param ctx ssl_ctx
---@param options? {server: boolean?, servername: string?}
local function newSecureSocket(socket, ctx, options)
options = options or {}
local ssocket = {
handle = socket, -- the wrapped stream
tls = true, -- distinguish secure sockets from normal ones
connected = false, -- whether the handshake & verification is done and we're ready for data

local function flush(callback)
local chunks = {}
local i = 0
while bout:pending() > 0 do
i = i + 1
chunks[i] = bout:read()
end
if i == 0 then
if callback then callback() end
return true
end
return socket:write(chunks, callback)
end

local function handshake(callback)
if ssl:handshake() then
local success, result = ssl:getpeerverification()
socket:read_stop()
if not success and result then
for i=1, #result do
if not result[i].preverify_ok then
handshakeComplete("Error verifying peer: " .. result[i].error_string)
return closeSocket(socket)
end
end
end
isServer = options.server, -- whether we're talking to a server (peer is a server)
servername = options.servername, -- the name of the server we're talking to (domain) if any

if not isServer then
local cert = ssl:peer()
if not cert then
handshakeComplete("The peer did not provide a certificate")
return closeSocket(socket)
end
if not cert:check_host(servername) then
handshakeComplete("The server hostname does not match the certificate's domain")
return closeSocket(socket)
end
end
bin = openssl.bio.mem(BIO_BUFFER_SIZE), -- bio input buffer
bout = openssl.bio.mem(BIO_BUFFER_SIZE), -- bio output buffer

handshakeComplete(nil, ssocket)
end
return flush(callback)
end
ssl = nil, -- the SSL session object
onPlain = nil, -- the reader assigned for the incoming decrypted stream
onCipher = nil, -- the reader assigned for the incoming encrypted stream
onHandshakeComplete = nil, -- called when the handshake exchange is done
}

local function onCipher(err, data)
if not onPlain then
if err or not data then
return handshakeComplete(err or "Peer aborted the SSL handshake", data)
end
bin:write(data)
return handshake()
end
if err or not data then
return onPlain(err, data)
end
bin:write(data)
while true do
local plain = ssl:read()
if not plain then break end
onPlain(nil, plain)
end
end
ssocket.ssl = ctx:ssl(ssocket.bin, ssocket.bout, ssocket.isServer)

-- When requested to start reading, start the real socket and setup
-- onPlain handler
-- the onPlain handler
function ssocket.read_start(_, onRead)
onPlain = onRead
return socket:read_start(onCipher)
ssocket.onPlain = onRead
local success, err = socket:read_start(ssocket.onCipher)
-- if we have data already available read it, see #341.
-- we have to delay the callback to the next tick after we return
-- so the caller has a chance to handle incoming data.
if success then
if ssocket.connected and ssocket.ssl:peek(PEEK_LENGTH) then
uv.new_timer():start(0, 0, function()
readIncoming(ssocket)
end)
end
end
return success, err
end

-- When requested to write plain data, encrypt it and write to socket
function ssocket.write(_, plain, callback)
ssl:write(plain)
return flush(callback)
ssocket.ssl:write(plain) -- TODO: handle write errors
return flushSecureSocket(ssocket, callback)
end

-- Make the wrapped stream methods available
-- the result methods doesn't depend on `self`
setmetatable(ssocket, {
__index = function(t, k)
local ov = rawget(t, k)
local tsocket = rawget(t, "handle")
if not ov and tsocket and tsocket[k] ~= nil then
if type(tsocket[k]) == "function" then
return wrapSocketMethod(tsocket, tsocket[k])
else
return tsocket[k]
end
else
return ov
end
end
})

return ssocket
end

local function doPeerVerification(ssocket)
local success, result = ssocket.ssl:getpeerverification()
if not success and result then
for i=1, #result do
if not result[i].preverify_ok then
closeSocket(ssocket.handle)
return nil, "Error verifying peer: " .. result[i].error_string
end
end
else
return true, result
end
end

function ssocket.shutdown(_, ...)
return socket:shutdown(...)
local function doPeerCertValidation(ssocket)
local cert = ssocket.ssl:peer()
if not cert then
return nil, "The peer did not provide a certificate"
end
function ssocket.read_stop(_, ...)
return socket:read_stop(...)
if not cert:check_host(ssocket.servername) then
return nil, "The server hostname does not match the certificate's domain"
end
function ssocket.is_closing(_, ...)
return socket:is_closing(...)
return true
end

local function doHandshake(ssocket)
-- TODO: optimize handshakes by implementing sessions
-- TODO: handle handshake errors properly and reattempt handshake when requested to
if not ssocket.ssl:handshake() then
return flushSecureSocket(ssocket)
end
function ssocket.close(_, ...)
return socket:close(...)

ssocket.handle:read_stop()
local success, result = doPeerVerification(ssocket)
if not success then
closeSocket(ssocket.handle)
return ssocket.onHandshakeComplete(result)
end
function ssocket.unref(_, ...)
return socket:unref(...)

if not ssocket.isServer then
success, result = doPeerCertValidation(ssocket)
if not success then
closeSocket(ssocket.handle)
return ssocket.onHandshakeComplete(result)
end
end
function ssocket.ref(_, ...)
return socket:ref(...)
ssocket.connected = true

return ssocket.onHandshakeComplete(nil, ssocket)
end

---@param ctx ssl_ctx
---@param socket uv_stream_t
---@param options {server: boolean?, servername: string?}
---@param handshakeComplete function # called when the handshake is complete and it's safe
return function (ctx, socket, options, handshakeComplete)
local ssocket = newSecureSocket(socket, ctx, options)
ssocket.onHandshakeComplete = handshakeComplete

if not options.server and options.servername then
ssocket.ssl:set("hostname", options.servername)
end

handshake()
socket:read_start(onCipher)
local function onCipher(err, data)
if not ssocket.connected then
if err or not data then
return handshakeComplete(err or "Peer aborted the SSL handshake", data)
end
ssocket.bin:write(data)
return doHandshake(ssocket)
end
if err or not data then
return ssocket.onPlain(err, data)
end
ssocket.bin:write(data)
readIncoming(ssocket)
end
ssocket.onCipher = onCipher

doHandshake(ssocket)
socket:read_start(onCipher)
end
4 changes: 2 additions & 2 deletions deps/secure-socket/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ return function (socket, options, callback)
if not callback then
thread = coroutine.running()
end
bioWrap(ctx, options.server, socket, callback or function (err, ssocket)
bioWrap(ctx, socket, options, callback or function (err, ssocket)
return assertResume(thread, ssocket, err)
end, options.servername)
end)
if not callback then
return coroutine.yield()
end
Expand Down
4 changes: 2 additions & 2 deletions deps/secure-socket/package.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
return {
name = "luvit/secure-socket",
version = "1.2.4",
homepage = "https://github.com/luvit/luvit/blob/master/deps/secure-socket",
version = "1.2.5",
homepage = "https://github.com/luvit/lit/blob/master/deps/secure-socket",
description = "Wrapper for luv streams to apply ssl/tls",
dependencies = {
"luvit/[email protected]"
Expand Down