Add native TLS for Android love.system and desktop gen1tls.

Expose a non-blocking TLS socket API to mods (WSS clients) without bundling
any game-specific multiworld content.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Solidus Snake
2026-08-10 17:43:53 -04:00
parent 4d03a38067
commit a9767e5df1
15 changed files with 1267 additions and 0 deletions
@@ -23,6 +23,7 @@
#ifdef LOVE_ANDROID
#include <cerrno>
#include <cstring>
#include <unordered_map>
#include <SDL.h>
@@ -324,6 +325,182 @@ bool httpDownload(const char *url, const char *destPath, const char *userAgent,
return result;
}
/*
* TLS sockets. Same resolution rule as httpDownload above -- the activity's
* own class, never FindClass -- and the same tolerance for an old APK: a
* missing method answers like a platform without TLS instead of aborting.
*/
static jclass tlsActivityClass(JNIEnv *env)
{
jobject activityObj = (jobject) SDL_AndroidGetActivity();
if (activityObj == nullptr)
return nullptr;
jclass activity = env->GetObjectClass(activityObj);
env->DeleteLocalRef(activityObj);
return activity;
}
int tlsOpen(const char *host, int port)
{
if (host == nullptr)
return -1;
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
jclass activity = tlsActivityClass(env);
if (activity == nullptr)
return -1;
jmethodID method = env->GetStaticMethodID(activity, "tlsOpen", "(Ljava/lang/String;I)I");
if (method == nullptr)
{
env->ExceptionClear();
env->DeleteLocalRef(activity);
return -1;
}
jstring jhost = env->NewStringUTF(host);
jint result = env->CallStaticIntMethod(activity, method, jhost, (jint) port);
env->DeleteLocalRef(jhost);
env->DeleteLocalRef(activity);
return (int) result;
}
int tlsStatus(int handle)
{
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
jclass activity = tlsActivityClass(env);
if (activity == nullptr)
return -1;
jmethodID method = env->GetStaticMethodID(activity, "tlsStatus", "(I)I");
if (method == nullptr)
{
env->ExceptionClear();
env->DeleteLocalRef(activity);
return -1;
}
jint result = env->CallStaticIntMethod(activity, method, (jint) handle);
env->DeleteLocalRef(activity);
return (int) result;
}
int tlsSend(int handle, const char *data, int length)
{
if (data == nullptr || length <= 0)
return 0;
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
jclass activity = tlsActivityClass(env);
if (activity == nullptr)
return -1;
jmethodID method = env->GetStaticMethodID(activity, "tlsSend", "(I[B)I");
if (method == nullptr)
{
env->ExceptionClear();
env->DeleteLocalRef(activity);
return -1;
}
jbyteArray payload = env->NewByteArray((jsize) length);
if (payload == nullptr)
{
env->ExceptionClear();
env->DeleteLocalRef(activity);
return -1;
}
env->SetByteArrayRegion(payload, 0, (jsize) length, (const jbyte*) data);
jint result = env->CallStaticIntMethod(activity, method, (jint) handle, payload);
env->DeleteLocalRef(payload);
env->DeleteLocalRef(activity);
return (int) result;
}
int tlsReceive(int handle, char *buf, int max)
{
if (buf == nullptr || max <= 0)
return 0;
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
jclass activity = tlsActivityClass(env);
if (activity == nullptr)
return -1;
jmethodID method = env->GetStaticMethodID(activity, "tlsReceive", "(II)[B");
if (method == nullptr)
{
env->ExceptionClear();
env->DeleteLocalRef(activity);
return -1;
}
jobject result = env->CallStaticObjectMethod(activity, method, (jint) handle, (jint) max);
env->DeleteLocalRef(activity);
if (result == nullptr)
return 0;
jbyteArray bytes = (jbyteArray) result;
jsize length = env->GetArrayLength(bytes);
if (length > max)
length = max;
env->GetByteArrayRegion(bytes, 0, length, (jbyte*) buf);
env->DeleteLocalRef(result);
return (int) length;
}
bool tlsError(int handle, char *buf, int max)
{
if (buf == nullptr || max <= 0)
return false;
buf[0] = '\0';
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
jclass activity = tlsActivityClass(env);
if (activity == nullptr)
return false;
jmethodID method = env->GetStaticMethodID(activity, "tlsError", "(I)Ljava/lang/String;");
if (method == nullptr)
{
env->ExceptionClear();
env->DeleteLocalRef(activity);
return false;
}
jobject result = env->CallStaticObjectMethod(activity, method, (jint) handle);
env->DeleteLocalRef(activity);
if (result == nullptr)
return false;
jstring text = (jstring) result;
const char *utf = env->GetStringUTFChars(text, nullptr);
if (utf != nullptr)
{
strncpy(buf, utf, (size_t) max - 1);
buf[max - 1] = '\0';
env->ReleaseStringUTFChars(text, utf);
}
env->DeleteLocalRef(result);
return buf[0] != '\0';
}
void tlsClose(int handle)
{
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
jclass activity = tlsActivityClass(env);
if (activity == nullptr)
return;
jmethodID method = env->GetStaticMethodID(activity, "tlsClose", "(I)V");
if (method == nullptr)
{
env->ExceptionClear();
env->DeleteLocalRef(activity);
return;
}
env->CallStaticVoidMethod(activity, method, (jint) handle);
env->DeleteLocalRef(activity);
}
/*
* Helper functions for the filesystem module
*/
@@ -98,6 +98,27 @@ bool restartApp();
**/
bool httpDownload(const char *url, const char *destPath, const char *userAgent, const char *accept);
/**
* TLS client sockets (GameActivity.tls*, implemented by TlsSocket.java).
* LuaSocket, which is what LOVE ships, does TCP only, so wss:// is otherwise
* unreachable -- and an Archipelago room hosted on archipelago.gg accepts a
* plain connection only to drop it. The platform has both a TLS stack and the
* system trust store, so this borrows them rather than vendoring mbedTLS.
*
* tlsOpen returns a handle immediately and connects on its own thread: poll
* tlsStatus for 0 connecting / 1 open / 2 closed, and -1 for a handle that
* does not exist. Bytes given to tlsSend before the handshake finishes are
* queued rather than refused. tlsReceive fills buf and returns how much it
* took, 0 when nothing is waiting. A closed connection keeps both its reason
* (tlsError) and whatever arrived before it closed until tlsClose.
**/
int tlsOpen(const char *host, int port);
int tlsStatus(int handle);
int tlsSend(int handle, const char *data, int length);
int tlsReceive(int handle, char *buf, int max);
bool tlsError(int handle, char *buf, int max);
void tlsClose(int handle);
/*
* Helper functions for the filesystem module
*/
@@ -244,6 +244,72 @@ bool System::httpDownload(const char *url, const char *destPath,
#endif
}
int System::tlsOpen(const char *host, int port) const
{
#ifdef LOVE_ANDROID
return love::android::tlsOpen(host, port);
#else
LOVE_UNUSED(host);
LOVE_UNUSED(port);
return -1;
#endif
}
int System::tlsStatus(int handle) const
{
#ifdef LOVE_ANDROID
return love::android::tlsStatus(handle);
#else
LOVE_UNUSED(handle);
return -1;
#endif
}
int System::tlsSend(int handle, const char *data, int length) const
{
#ifdef LOVE_ANDROID
return love::android::tlsSend(handle, data, length);
#else
LOVE_UNUSED(handle);
LOVE_UNUSED(data);
LOVE_UNUSED(length);
return -1;
#endif
}
int System::tlsReceive(int handle, char *buf, int max) const
{
#ifdef LOVE_ANDROID
return love::android::tlsReceive(handle, buf, max);
#else
LOVE_UNUSED(handle);
LOVE_UNUSED(buf);
LOVE_UNUSED(max);
return -1;
#endif
}
bool System::tlsError(int handle, char *buf, int max) const
{
#ifdef LOVE_ANDROID
return love::android::tlsError(handle, buf, max);
#else
LOVE_UNUSED(handle);
LOVE_UNUSED(buf);
LOVE_UNUSED(max);
return false;
#endif
}
void System::tlsClose(int handle) const
{
#ifdef LOVE_ANDROID
love::android::tlsClose(handle);
#else
LOVE_UNUSED(handle);
#endif
}
bool System::hasBackgroundMusic() const
{
#if defined(LOVE_ANDROID)
@@ -149,6 +149,20 @@ public:
virtual bool httpDownload(const char *url, const char *destPath,
const char *userAgent = nullptr, const char *accept = nullptr) const;
/**
* TLS client sockets (Android only; every call fails elsewhere, where
* LuaSec or another provider is the answer). Non-blocking by contract:
* tlsOpen returns a handle and connects on its own thread, tlsStatus
* reports 0 connecting / 1 open / 2 closed / -1 unknown, and bytes sent
* before the handshake completes are queued rather than refused.
**/
virtual int tlsOpen(const char *host, int port) const;
virtual int tlsStatus(int handle) const;
virtual int tlsSend(int handle, const char *data, int length) const;
virtual int tlsReceive(int handle, char *buf, int max) const;
virtual bool tlsError(int handle, char *buf, int max) const;
virtual void tlsClose(int handle) const;
/**
* Gets if the user is playing music on background.
* Throws an exception on unsupported platforms.
@@ -139,6 +139,79 @@ int w_hasBackgroundMusic(lua_State *L)
return 1;
}
/*
* TLS sockets. Deliberately a handle-and-poll API rather than an object:
* the caller is a per-frame pump that must never block, and everything with
* a thread behind it lives on the Java side.
*/
int w_tlsOpen(lua_State *L)
{
const char *host = luaL_checkstring(L, 1);
int port = (int) luaL_checknumber(L, 2);
lua_pushnumber(L, instance()->tlsOpen(host, port));
return 1;
}
int w_tlsStatus(lua_State *L)
{
int handle = (int) luaL_checknumber(L, 1);
lua_pushnumber(L, instance()->tlsStatus(handle));
return 1;
}
int w_tlsSend(lua_State *L)
{
int handle = (int) luaL_checknumber(L, 1);
size_t length = 0;
const char *data = luaL_checklstring(L, 2, &length);
lua_pushnumber(L, instance()->tlsSend(handle, data, (int) length));
return 1;
}
int w_tlsReceive(lua_State *L)
{
int handle = (int) luaL_checknumber(L, 1);
int max = (int) luaL_optnumber(L, 2, 8192);
if (max <= 0)
{
lua_pushliteral(L, "");
return 1;
}
// A frame's worth of a busy room, on the C stack rather than the heap:
// this runs every frame and an allocation per poll is not worth it.
if (max > 65536)
max = 65536;
char buf[65536];
int got = instance()->tlsReceive(handle, buf, max);
if (got < 0)
{
lua_pushnil(L);
return 1;
}
lua_pushlstring(L, buf, (size_t) got);
return 1;
}
int w_tlsError(lua_State *L)
{
int handle = (int) luaL_checknumber(L, 1);
char buf[512];
if (!instance()->tlsError(handle, buf, (int) sizeof(buf)))
{
lua_pushnil(L);
return 1;
}
lua_pushstring(L, buf);
return 1;
}
int w_tlsClose(lua_State *L)
{
int handle = (int) luaL_checknumber(L, 1);
instance()->tlsClose(handle);
return 0;
}
static const luaL_Reg functions[] =
{
{ "getOS", w_getOS },
@@ -153,6 +226,12 @@ static const luaL_Reg functions[] =
{ "syncHealthSteps", w_syncHealthSteps },
{ "restartApp", w_restartApp },
{ "httpDownload", w_httpDownload },
{ "tlsOpen", w_tlsOpen },
{ "tlsStatus", w_tlsStatus },
{ "tlsSend", w_tlsSend },
{ "tlsReceive", w_tlsReceive },
{ "tlsError", w_tlsError },
{ "tlsClose", w_tlsClose },
{ "hasBackgroundMusic", w_hasBackgroundMusic },
{ 0, 0 }
};