1
0

LuaAPI: Added client TLS support for TCP links.

This commit is contained in:
Mattes D 2015-02-12 20:05:55 +01:00
parent 086e112161
commit 16636ff6e2
12 changed files with 395 additions and 8 deletions

View File

@ -282,7 +282,15 @@ g_Server = nil
calling {{cNetwork}}:Connect() to connect to a remote server, or by listening using
{{cNetwork}}:Listen() and accepting incoming connections. The links are callback-based - when an event
such as incoming data or remote disconnect happens on the link, a specific callback is called. See the
additional information in {{cNetwork}} documentation for details.
additional information in {{cNetwork}} documentation for details.</p>
<p>
The link can also optionally perform TLS encryption. Plugins can use the StartTLSClient() function to
start the TLS handshake as the client side. Since that call, the OnReceivedData() callback is
overridden internally so that the data is first routed through the TLS decryptor, and the plugin's
callback is only called for the decrypted data, once it starts arriving. The Send function changes its
behavior so that the data written by the plugin is first encrypted and only then sent over the
network. Note that calling Send() before the TLS handshake finishes is supported, but the data is
queued internally and only sent once the TLS handshake is completed.
]],
Functions =
@ -292,6 +300,7 @@ g_Server = nil
GetRemoteIP = { Params = "", Return = "string", Notes = "Returns the IP address of the remote endpoint of the TCP connection." },
GetRemotePort = { Params = "", Return = "number", Notes = "Returns the port of the remote endpoint of the TCP connection." },
Send = { Params = "Data", Return = "", Notes = "Sends the data (raw string) to the remote peer. The data is sent asynchronously and there is no report on the success of the send operation, other than the connection being closed or reset by the underlying OS." },
StartTLSClient = { Params = "OwnCert, OwnPrivateKey, OwnPrivateKeyPassword", Return = "", Notes = "Starts a TLS handshake on the link, as a client side of the TLS. The Own___ parameters specify the client certificate and its corresponding private key and password; all three parameters are optional and no client certificate is presented to the remote peer if they are not used or all empty. Once the TLS handshake is started by this call, all incoming data is first decrypted before being sent to the OnReceivedData callback, and all outgoing data is queued until the TLS handshake completes, and then sent encrypted over the link." },
},
}, -- cTCPLink

View File

@ -84,6 +84,12 @@ g_PluginInfo =
},
}, -- lookup
wasc =
{
HelpString = "Requests the webadmin homepage using https",
Handler = HandleConsoleNetWasc,
}, -- wasc
}, -- Subcommands
}, -- net
},

View File

@ -252,3 +252,47 @@ end
function HandleConsoleNetWasc(a_Split)
local Callbacks =
{
OnConnected = function (a_Link)
LOG("Connected to webadmin, starting TLS...")
local res, msg = a_Link:StartTLSClient("", "", "")
if not(res) then
LOG("Failed to start TLS client: " .. msg)
return
end
-- We need to send a keep-alive due to #1737
a_Link:Send("GET / HTTP/1.0\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n")
end,
OnError = function (a_Link, a_ErrorCode, a_ErrorMsg)
LOG("Connection to webadmin failed: " .. a_ErrorCode .. " (" .. a_ErrorMsg .. ")")
end,
OnReceivedData = function (a_Link, a_Data)
LOG("Received data from webadmin:\r\n" .. a_Data)
-- Close the link once all the data is received:
if (a_Data == "0\r\n\r\n") then -- Poor man's end-of-data detection; works on localhost
-- TODO: The Close() method is not yet exported to Lua
-- a_Link:Close()
end
end,
OnRemoteClosed = function (a_Link)
LOG("Connection to webadmin was closed")
end,
}
if not(cNetwork:Connect("localhost", "8080", Callbacks)) then
LOG("Canot connect to webadmin")
end
return true
end

View File

@ -343,6 +343,18 @@ bool cLuaState::PushFunction(const cTableRef & a_TableRef)
void cLuaState::PushNil(void)
{
ASSERT(IsValid());
lua_pushnil(m_LuaState);
m_NumCurrentFunctionArgs += 1;
}
void cLuaState::Push(const AString & a_String)
{
ASSERT(IsValid());

View File

@ -184,6 +184,8 @@ public:
/** Returns true if a_FunctionName is a valid Lua function that can be called */
bool HasFunction(const char * a_FunctionName);
void PushNil(void);
// Push a const value onto the stack (keep alpha-sorted):
void Push(const AString & a_String);
void Push(const AStringVector & a_Vector);

View File

@ -64,6 +64,13 @@ cLuaTCPLink::~cLuaTCPLink()
bool cLuaTCPLink::Send(const AString & a_Data)
{
// If running in SSL mode, push the data into the SSL context instead:
if (m_SslContext != nullptr)
{
m_SslContext->Send(a_Data);
return true;
}
// Safely grab a copy of the link:
cTCPLinkPtr Link = m_Link;
if (Link == nullptr)
@ -179,6 +186,58 @@ void cLuaTCPLink::Close(void)
AString cLuaTCPLink::StartTLSClient(
const AString & a_OwnCertData,
const AString & a_OwnPrivKeyData,
const AString & a_OwnPrivKeyPassword
)
{
// Check preconditions:
if (m_SslContext != nullptr)
{
return "TLS is already active on this link";
}
if (
(a_OwnCertData.empty() && !a_OwnPrivKeyData.empty()) ||
(!a_OwnCertData.empty() && a_OwnPrivKeyData.empty())
)
{
return "Either provide both the certificate and private key, or neither";
}
// Create the SSL context:
m_SslContext = std::make_unique<cLinkSslContext>(*this);
m_SslContext->Initialize(true);
// Create the peer cert, if required:
if (!a_OwnCertData.empty() && !a_OwnPrivKeyData.empty())
{
auto OwnCert = std::make_shared<cX509Cert>();
int res = OwnCert->Parse(a_OwnCertData.data(), a_OwnCertData.size());
if (res != 0)
{
m_SslContext.reset();
return Printf("Cannot parse peer certificate: -0x%x", res);
}
auto OwnPrivKey = std::make_shared<cCryptoKey>();
res = OwnPrivKey->ParsePrivate(a_OwnPrivKeyData.data(), a_OwnPrivKeyData.size(), a_OwnPrivKeyPassword);
if (res != 0)
{
m_SslContext.reset();
return Printf("Cannot parse peer private key: -0x%x", res);
}
m_SslContext->SetOwnCert(OwnCert, OwnPrivKey);
}
// Start the handshake:
m_SslContext->Handshake();
return "";
}
void cLuaTCPLink::Terminated(void)
{
// Disable the callbacks:
@ -207,6 +266,26 @@ void cLuaTCPLink::Terminated(void)
void cLuaTCPLink::ReceivedCleartextData(const char * a_Data, size_t a_NumBytes)
{
// Check if we're still valid:
if (!m_Callbacks.IsValid())
{
return;
}
// Call the callback:
cPluginLua::cOperation Op(m_Plugin);
if (!Op().Call(cLuaState::cTableRef(m_Callbacks, "OnReceivedData"), this, AString(a_Data, a_NumBytes)))
{
LOGINFO("cTCPLink OnReceivedData callback failed in plugin %s.", m_Plugin.GetName().c_str());
}
}
void cLuaTCPLink::OnConnected(cTCPLink & a_Link)
{
// Check if we're still valid:
@ -269,6 +348,13 @@ void cLuaTCPLink::OnReceivedData(const char * a_Data, size_t a_Length)
return;
}
// If we're running in SSL mode, put the data into the SSL decryptor:
if (m_SslContext != nullptr)
{
m_SslContext->StoreReceivedData(a_Data, a_Length);
return;
}
// Call the callback:
cPluginLua::cOperation Op(m_Plugin);
if (!Op().Call(cLuaState::cTableRef(m_Callbacks, "OnReceivedData"), this, AString(a_Data, a_Length)))
@ -302,3 +388,118 @@ void cLuaTCPLink::OnRemoteClosed(void)
////////////////////////////////////////////////////////////////////////////////
// cLuaTCPLink::cLinkSslContext:
cLuaTCPLink::cLinkSslContext::cLinkSslContext(cLuaTCPLink & a_Link):
m_Link(a_Link)
{
}
void cLuaTCPLink::cLinkSslContext::StoreReceivedData(const char * a_Data, size_t a_NumBytes)
{
m_EncryptedData.append(a_Data, a_NumBytes);
// Try to finish a pending handshake:
TryFinishHandshaking();
// Flush any cleartext data that can be "received":
FlushBuffers();
}
void cLuaTCPLink::cLinkSslContext::FlushBuffers(void)
{
// If the handshake didn't complete yet, bail out:
if (!HasHandshaken())
{
return;
}
char Buffer[1024];
int NumBytes;
while ((NumBytes = ReadPlain(Buffer, sizeof(Buffer))) > 0)
{
m_Link.ReceivedCleartextData(Buffer, static_cast<size_t>(NumBytes));
}
}
void cLuaTCPLink::cLinkSslContext::TryFinishHandshaking(void)
{
// If the handshake hasn't finished yet, retry:
if (!HasHandshaken())
{
Handshake();
}
// If the handshake succeeded, write all the queued plaintext data:
if (HasHandshaken())
{
WritePlain(m_CleartextData.data(), m_CleartextData.size());
m_CleartextData.clear();
}
}
void cLuaTCPLink::cLinkSslContext::Send(const AString & a_Data)
{
// If the handshake hasn't completed yet, queue the data:
if (!HasHandshaken())
{
m_CleartextData.append(a_Data);
TryFinishHandshaking();
return;
}
// The connection is all set up, write the cleartext data into the SSL context:
WritePlain(a_Data.data(), a_Data.size());
FlushBuffers();
}
int cLuaTCPLink::cLinkSslContext::ReceiveEncrypted(unsigned char * a_Buffer, size_t a_NumBytes)
{
// If there's nothing queued in the buffer, report empty buffer:
if (m_EncryptedData.empty())
{
return POLARSSL_ERR_NET_WANT_READ;
}
// Copy as much data as possible to the provided buffer:
size_t BytesToCopy = std::min(a_NumBytes, m_EncryptedData.size());
memcpy(a_Buffer, m_EncryptedData.data(), BytesToCopy);
m_EncryptedData.erase(0, BytesToCopy);
return static_cast<int>(BytesToCopy);
}
int cLuaTCPLink::cLinkSslContext::SendEncrypted(const unsigned char * a_Buffer, size_t a_NumBytes)
{
m_Link.m_Link->Send(a_Buffer, a_NumBytes);
return static_cast<int>(a_NumBytes);
}

View File

@ -11,6 +11,7 @@
#include "../OSSupport/Network.h"
#include "PluginLua.h"
#include "../PolarSSL++/SslContext.h"
@ -62,7 +63,53 @@ public:
Sends the RST packet, queued outgoing and incoming data is lost. */
void Close(void);
/** Starts a TLS handshake as a client connection.
If a client certificate should be used for the connection, set the certificate into a_OwnCertData and
its corresponding private key to a_OwnPrivKeyData. If both are empty, no client cert is presented.
a_OwnPrivKeyPassword is the password to be used for decoding PrivKey, empty if not passworded.
Returns empty string on success, non-empty error description on failure. */
AString StartTLSClient(
const AString & a_OwnCertData,
const AString & a_OwnPrivKeyData,
const AString & a_OwnPrivKeyPassword
);
protected:
/** Wrapper around cSslContext that is used when this link is being encrypted by SSL. */
class cLinkSslContext :
public cSslContext
{
cLuaTCPLink & m_Link;
/** Buffer for storing the incoming encrypted data until it is requested by the SSL decryptor. */
AString m_EncryptedData;
/** Buffer for storing the outgoing cleartext data until the link has finished handshaking. */
AString m_CleartextData;
public:
cLinkSslContext(cLuaTCPLink & a_Link);
/** Stores the specified block of data into the buffer of the data to be decrypted (incoming from remote).
Also flushes the SSL buffers by attempting to read any data through the SSL context. */
void StoreReceivedData(const char * a_Data, size_t a_NumBytes);
/** Tries to read any cleartext data available through the SSL, reports it in the link. */
void FlushBuffers(void);
/** Tries to finish handshaking the SSL. */
void TryFinishHandshaking(void);
/** Sends the specified cleartext data over the SSL to the remote peer.
If the handshake hasn't been completed yet, queues the data for sending when it completes. */
void Send(const AString & a_Data);
// cSslContext overrides:
virtual int ReceiveEncrypted(unsigned char * a_Buffer, size_t a_NumBytes) override;
virtual int SendEncrypted(const unsigned char * a_Buffer, size_t a_NumBytes) override;
};
/** The plugin for which the link is created. */
cPluginLua & m_Plugin;
@ -76,11 +123,19 @@ protected:
/** The server that is responsible for this link, if any. */
cLuaServerHandleWPtr m_Server;
/** The SSL context used for encryption, if this link uses SSL.
If valid, the link uses encryption through this context. */
UniquePtr<cLinkSslContext> m_SslContext;
/** Common code called when the link is considered as terminated.
Releases m_Link, m_Callbacks and this from m_Server, each when applicable. */
void Terminated(void);
/** Called by the SSL context when there's incoming data available in the cleartext.
Reports the data via the Lua callback function. */
void ReceivedCleartextData(const char * a_Data, size_t a_NumBytes);
// cNetwork::cConnectCallbacks overrides:
virtual void OnConnected(cTCPLink & a_Link) override;
virtual void OnError(int a_ErrorCode, const AString & a_ErrorMsg) override;

View File

@ -389,6 +389,51 @@ static int tolua_cTCPLink_GetRemotePort(lua_State * L)
/** Binds cLuaTCPLink::StartTLSClient */
static int tolua_cTCPLink_StartTLSClient(lua_State * L)
{
// Function signature:
// LinkInstance:StartTLSClient(OwnCert, OwnPrivKey, OwnPrivKeyPassword) -> [true] or [nil, ErrMsg]
cLuaState S(L);
if (
!S.CheckParamUserType(1, "cTCPLink") ||
!S.CheckParamString(2, 4) ||
!S.CheckParamEnd(5)
)
{
return 0;
}
// Get the link:
cLuaTCPLink * Link;
if (lua_isnil(L, 1))
{
LOGWARNING("cTCPLink:StartTLSClient(): invalid link object. Stack trace:");
S.LogStackTrace();
return 0;
}
Link = *static_cast<cLuaTCPLink **>(lua_touserdata(L, 1));
// Read the params:
AString OwnCert, OwnPrivKey, OwnPrivKeyPassword;
S.GetStackValues(2, OwnCert, OwnPrivKey, OwnPrivKeyPassword);
// Start the TLS handshake:
AString res = Link->StartTLSClient(OwnCert, OwnPrivKey, OwnPrivKeyPassword);
if (!res.empty())
{
S.PushNil();
S.Push(Printf("Cannot start TLS on link to %s:%d: %s", Link->GetRemoteIP().c_str(), Link->GetRemotePort(), res.c_str()));
return 2;
}
return 1;
}
////////////////////////////////////////////////////////////////////////////////
// cServerHandle bindings (routed through cLuaServerHandle):
@ -495,11 +540,12 @@ void ManualBindings::BindNetwork(lua_State * tolua_S)
tolua_endmodule(tolua_S);
tolua_beginmodule(tolua_S, "cTCPLink");
tolua_function(tolua_S, "Send", tolua_cTCPLink_Send);
tolua_function(tolua_S, "GetLocalIP", tolua_cTCPLink_GetLocalIP);
tolua_function(tolua_S, "GetLocalPort", tolua_cTCPLink_GetLocalPort);
tolua_function(tolua_S, "GetRemoteIP", tolua_cTCPLink_GetRemoteIP);
tolua_function(tolua_S, "GetRemotePort", tolua_cTCPLink_GetRemotePort);
tolua_function(tolua_S, "Send", tolua_cTCPLink_Send);
tolua_function(tolua_S, "GetLocalIP", tolua_cTCPLink_GetLocalIP);
tolua_function(tolua_S, "GetLocalPort", tolua_cTCPLink_GetLocalPort);
tolua_function(tolua_S, "GetRemoteIP", tolua_cTCPLink_GetRemoteIP);
tolua_function(tolua_S, "GetRemotePort", tolua_cTCPLink_GetRemotePort);
tolua_function(tolua_S, "StartTLSClient", tolua_cTCPLink_StartTLSClient);
tolua_endmodule(tolua_S);
tolua_beginmodule(tolua_S, "cServerHandle");

View File

@ -379,9 +379,10 @@ void inline LOG(const char * a_Format, ...)
#define assert_test(x) ( !!(x) || (assert(!#x), exit(1), 0))
#endif
// Unified shared ptr from before C++11. Also no silly undercores.
// Unified ptr types from before C++11. Also no silly undercores.
#define SharedPtr std::shared_ptr
#define WeakPtr std::weak_ptr
#define UniquePtr std::unique_ptr

View File

@ -25,6 +25,15 @@ cSslHTTPConnection::cSslHTTPConnection(cHTTPServer & a_HTTPServer, const cX509Ce
cSslHTTPConnection::~cSslHTTPConnection()
{
m_Ssl.NotifyClose();
}
void cSslHTTPConnection::OnReceivedData(const char * a_Data, size_t a_Size)
{
// Process the received data:

View File

@ -25,6 +25,8 @@ public:
/** Creates a new connection on the specified server.
Sends the specified cert as the server certificate, uses the private key for decryption. */
cSslHTTPConnection(cHTTPServer & a_HTTPServer, const cX509CertPtr & a_Cert, const cCryptoKeyPtr & a_PrivateKey);
~cSslHTTPConnection();
protected:
cBufferedSslContext m_Ssl;

View File

@ -45,7 +45,7 @@ cCryptoKey::cCryptoKey(const AString & a_PrivateKeyData, const AString & a_Passw
if (res != 0)
{
LOGWARNING("Failed to parse private key: -0x%x", res);
ASSERT(!"Cannot parse PubKey");
ASSERT(!"Cannot parse PrivKey");
return;
}
}