From 64d09ee7e765b95b209bfddee289d65a0491f759 Mon Sep 17 00:00:00 2001 From: Bilal2453 Date: Wed, 17 Sep 2025 20:49:49 +0300 Subject: [PATCH] secure-socket: fix #341 and refactor biowrap --- deps/secure-socket/biowrap.lua | 253 +++++++++++++++++++++------------ deps/secure-socket/init.lua | 4 +- deps/secure-socket/package.lua | 4 +- 3 files changed, 168 insertions(+), 93 deletions(-) diff --git a/deps/secure-socket/biowrap.lua b/deps/secure-socket/biowrap.lua index 56efb98..2713758 100644 --- a/deps/secure-socket/biowrap.lua +++ b/deps/secure-socket/biowrap.lua @@ -15,7 +15,11 @@ 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 @@ -23,117 +27,188 @@ local function closeSocket(socket) 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 diff --git a/deps/secure-socket/init.lua b/deps/secure-socket/init.lua index af1e74c..9715280 100644 --- a/deps/secure-socket/init.lua +++ b/deps/secure-socket/init.lua @@ -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 diff --git a/deps/secure-socket/package.lua b/deps/secure-socket/package.lua index 4afaca6..dd1d798 100644 --- a/deps/secure-socket/package.lua +++ b/deps/secure-socket/package.lua @@ -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/resource@2.1.0"