mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 13:09:54 +02:00
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:
@@ -68,3 +68,10 @@ mobile/ios/bundle_id.local
|
|||||||
/ports/uwp/build/
|
/ports/uwp/build/
|
||||||
/ports/uwp/third_party/*/source/
|
/ports/uwp/third_party/*/source/
|
||||||
/ports/uwp/third_party/angle/depot_tools/
|
/ports/uwp/third_party/angle/depot_tools/
|
||||||
|
|
||||||
|
# Native TLS dialer build output (dotnet publish)
|
||||||
|
/native/tls_dial/bin/
|
||||||
|
/native/tls_dial/obj/
|
||||||
|
/dist/native/
|
||||||
|
/dist/win/
|
||||||
|
/.bazinga/
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#ifdef LOVE_ANDROID
|
#ifdef LOVE_ANDROID
|
||||||
|
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
|
#include <cstring>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
@@ -324,6 +325,182 @@ bool httpDownload(const char *url, const char *destPath, const char *userAgent,
|
|||||||
return result;
|
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
|
* 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);
|
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
|
* Helper functions for the filesystem module
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -244,6 +244,72 @@ bool System::httpDownload(const char *url, const char *destPath,
|
|||||||
#endif
|
#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
|
bool System::hasBackgroundMusic() const
|
||||||
{
|
{
|
||||||
#if defined(LOVE_ANDROID)
|
#if defined(LOVE_ANDROID)
|
||||||
|
|||||||
@@ -149,6 +149,20 @@ public:
|
|||||||
virtual bool httpDownload(const char *url, const char *destPath,
|
virtual bool httpDownload(const char *url, const char *destPath,
|
||||||
const char *userAgent = nullptr, const char *accept = nullptr) const;
|
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.
|
* Gets if the user is playing music on background.
|
||||||
* Throws an exception on unsupported platforms.
|
* Throws an exception on unsupported platforms.
|
||||||
|
|||||||
@@ -139,6 +139,79 @@ int w_hasBackgroundMusic(lua_State *L)
|
|||||||
return 1;
|
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[] =
|
static const luaL_Reg functions[] =
|
||||||
{
|
{
|
||||||
{ "getOS", w_getOS },
|
{ "getOS", w_getOS },
|
||||||
@@ -153,6 +226,12 @@ static const luaL_Reg functions[] =
|
|||||||
{ "syncHealthSteps", w_syncHealthSteps },
|
{ "syncHealthSteps", w_syncHealthSteps },
|
||||||
{ "restartApp", w_restartApp },
|
{ "restartApp", w_restartApp },
|
||||||
{ "httpDownload", w_httpDownload },
|
{ "httpDownload", w_httpDownload },
|
||||||
|
{ "tlsOpen", w_tlsOpen },
|
||||||
|
{ "tlsStatus", w_tlsStatus },
|
||||||
|
{ "tlsSend", w_tlsSend },
|
||||||
|
{ "tlsReceive", w_tlsReceive },
|
||||||
|
{ "tlsError", w_tlsError },
|
||||||
|
{ "tlsClose", w_tlsClose },
|
||||||
{ "hasBackgroundMusic", w_hasBackgroundMusic },
|
{ "hasBackgroundMusic", w_hasBackgroundMusic },
|
||||||
{ 0, 0 }
|
{ 0, 0 }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -606,6 +606,45 @@ public class GameActivity extends SDLActivity {
|
|||||||
* The body lands in a .part file and is renamed only once complete, so a
|
* The body lands in a .part file and is renamed only once complete, so a
|
||||||
* dropped connection can never leave a half file the caller trusts.
|
* dropped connection can never leave a half file the caller trusts.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* TLS client sockets, exposed as love.system.tls* and used by the
|
||||||
|
* Archipelago mod for wss:// rooms. LuaSocket speaks TCP only, so without
|
||||||
|
* these a hosted room -- every one of which is TLS-only -- is unreachable
|
||||||
|
* from the game. The work is in TlsSocket; these are the static entry
|
||||||
|
* points, because the JNI side resolves methods on the activity's own
|
||||||
|
* class (see love/src/common/android.cpp) and cannot see other classes
|
||||||
|
* from a worker thread.
|
||||||
|
*/
|
||||||
|
@Keep
|
||||||
|
public static int tlsOpen(String host, int port) {
|
||||||
|
return TlsSocket.open(host, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public static int tlsStatus(int handle) {
|
||||||
|
return TlsSocket.status(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public static int tlsSend(int handle, byte[] data) {
|
||||||
|
return TlsSocket.send(handle, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public static byte[] tlsReceive(int handle, int max) {
|
||||||
|
return TlsSocket.receive(handle, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public static String tlsError(int handle) {
|
||||||
|
return TlsSocket.error(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public static void tlsClose(int handle) {
|
||||||
|
TlsSocket.close(handle);
|
||||||
|
}
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
public static boolean httpDownload(String url, String destPath, String userAgent, String accept) {
|
public static boolean httpDownload(String url, String destPath, String userAgent, String accept) {
|
||||||
if (url == null || destPath == null) return false;
|
if (url == null || destPath == null) return false;
|
||||||
|
|||||||
@@ -0,0 +1,310 @@
|
|||||||
|
package org.love2d.android;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
|
import javax.net.ssl.SNIHostName;
|
||||||
|
import javax.net.ssl.SNIServerName;
|
||||||
|
import javax.net.ssl.SSLParameters;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A TLS client socket that Lua can drive without ever blocking a frame.
|
||||||
|
*
|
||||||
|
* WHY THIS EXISTS. LuaSocket, which is what LOVE ships, speaks TCP and nothing
|
||||||
|
* else, so wss:// was simply unreachable from the game -- and every room hosted
|
||||||
|
* on archipelago.gg is TLS-only, accepting a plain connection just long enough
|
||||||
|
* to drop it. The alternative was vendoring mbedTLS into the NDK build and
|
||||||
|
* carrying a CA bundle in the APK; the platform already has both a TLS stack
|
||||||
|
* and the system trust store, so this asks Android instead.
|
||||||
|
*
|
||||||
|
* THE CONTRACT. Callers get an int handle and poll it. open() returns
|
||||||
|
* immediately and the connect and handshake happen on their own thread, so a
|
||||||
|
* slow or unreachable host costs nothing on the game thread -- which matters
|
||||||
|
* more here than it did for httpDownload, since that runs on a worker and this
|
||||||
|
* is serviced from the frame loop. Bytes handed to send() before the handshake
|
||||||
|
* finishes are queued, not refused, so a caller can write its request the
|
||||||
|
* moment it has a handle and never think about readiness again.
|
||||||
|
*
|
||||||
|
* Reads are drained by a thread into a chunk queue and handed over a copy at a
|
||||||
|
* time; a caller that stops polling stops the connection rather than growing
|
||||||
|
* the heap without limit.
|
||||||
|
*/
|
||||||
|
final class TlsSocket {
|
||||||
|
static final int STATUS_CONNECTING = 0;
|
||||||
|
static final int STATUS_OPEN = 1;
|
||||||
|
static final int STATUS_CLOSED = 2;
|
||||||
|
|
||||||
|
private static final int CONNECT_TIMEOUT_MS = 15000;
|
||||||
|
private static final int READ_CHUNK = 16384;
|
||||||
|
/** Roughly a second of a very chatty room; past this the reader is gone. */
|
||||||
|
private static final int MAX_BUFFERED = 4 * 1024 * 1024;
|
||||||
|
|
||||||
|
private static final ConcurrentHashMap<Integer, TlsSocket> LIVE =
|
||||||
|
new ConcurrentHashMap<Integer, TlsSocket>();
|
||||||
|
private static final AtomicInteger NEXT_HANDLE = new AtomicInteger(1);
|
||||||
|
|
||||||
|
private final String host;
|
||||||
|
private final int port;
|
||||||
|
private final int handle;
|
||||||
|
|
||||||
|
private volatile int status = STATUS_CONNECTING;
|
||||||
|
private volatile String error = null;
|
||||||
|
private volatile boolean closing = false;
|
||||||
|
private volatile SSLSocket socket = null;
|
||||||
|
|
||||||
|
private final Object inLock = new Object();
|
||||||
|
private final ArrayDeque<byte[]> inChunks = new ArrayDeque<byte[]>();
|
||||||
|
private int inHeadOffset = 0;
|
||||||
|
private int inAvailable = 0;
|
||||||
|
|
||||||
|
private final Object outLock = new Object();
|
||||||
|
private final ArrayDeque<byte[]> outChunks = new ArrayDeque<byte[]>();
|
||||||
|
|
||||||
|
private TlsSocket(String host, int port, int handle) {
|
||||||
|
this.host = host;
|
||||||
|
this.port = port;
|
||||||
|
this.handle = handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- API
|
||||||
|
|
||||||
|
static int open(String host, int port) {
|
||||||
|
if (host == null || host.length() == 0 || port <= 0 || port > 65535) return -1;
|
||||||
|
final int handle = NEXT_HANDLE.getAndIncrement();
|
||||||
|
final TlsSocket self = new TlsSocket(host, port, handle);
|
||||||
|
LIVE.put(Integer.valueOf(handle), self);
|
||||||
|
Thread dialer = new Thread(new Runnable() {
|
||||||
|
@Override public void run() { self.dial(); }
|
||||||
|
}, "tls-dial-" + handle);
|
||||||
|
dialer.setDaemon(true);
|
||||||
|
dialer.start();
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int status(int handle) {
|
||||||
|
TlsSocket self = LIVE.get(Integer.valueOf(handle));
|
||||||
|
return self == null ? -1 : self.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String error(int handle) {
|
||||||
|
TlsSocket self = LIVE.get(Integer.valueOf(handle));
|
||||||
|
return self == null ? null : self.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int send(int handle, byte[] data) {
|
||||||
|
TlsSocket self = LIVE.get(Integer.valueOf(handle));
|
||||||
|
if (self == null || data == null) return -1;
|
||||||
|
if (self.status == STATUS_CLOSED) return -1;
|
||||||
|
if (data.length == 0) return 0;
|
||||||
|
synchronized (self.outLock) {
|
||||||
|
self.outChunks.add(data);
|
||||||
|
self.outLock.notifyAll();
|
||||||
|
}
|
||||||
|
return data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] receive(int handle, int max) {
|
||||||
|
TlsSocket self = LIVE.get(Integer.valueOf(handle));
|
||||||
|
if (self == null || max <= 0) return null;
|
||||||
|
return self.take(max);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void close(int handle) {
|
||||||
|
TlsSocket self = LIVE.remove(Integer.valueOf(handle));
|
||||||
|
if (self != null) self.shutdown(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ internals
|
||||||
|
|
||||||
|
private void dial() {
|
||||||
|
Socket plain = null;
|
||||||
|
try {
|
||||||
|
plain = new Socket();
|
||||||
|
plain.connect(new InetSocketAddress(host, port), CONNECT_TIMEOUT_MS);
|
||||||
|
plain.setTcpNoDelay(true);
|
||||||
|
|
||||||
|
SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
|
||||||
|
SSLSocket ssl = (SSLSocket) factory.createSocket(plain, host, port, true);
|
||||||
|
|
||||||
|
// Wrapping an already-connected socket skips the SNI and hostname
|
||||||
|
// checking that createSocket(host, port) would have done for us, and
|
||||||
|
// a shared address like archipelago.gg answers with the wrong
|
||||||
|
// certificate without the name in the hello. Both are set through
|
||||||
|
// SSLParameters where the platform has it, with the verifier below
|
||||||
|
// as the floor for anything older.
|
||||||
|
boolean verifiedByPlatform = false;
|
||||||
|
try {
|
||||||
|
SSLParameters params = ssl.getSSLParameters();
|
||||||
|
params.setEndpointIdentificationAlgorithm("HTTPS");
|
||||||
|
List<SNIServerName> names = new ArrayList<SNIServerName>(1);
|
||||||
|
names.add(new SNIHostName(host));
|
||||||
|
params.setServerNames(names);
|
||||||
|
ssl.setSSLParameters(params);
|
||||||
|
verifiedByPlatform = true;
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// Older platform: handled after the handshake instead.
|
||||||
|
}
|
||||||
|
|
||||||
|
enableModernProtocols(ssl);
|
||||||
|
ssl.startHandshake();
|
||||||
|
|
||||||
|
if (!verifiedByPlatform
|
||||||
|
&& !HttpsURLConnection.getDefaultHostnameVerifier()
|
||||||
|
.verify(host, ssl.getSession())) {
|
||||||
|
throw new java.io.IOException(
|
||||||
|
"certificate does not match " + host);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket = ssl;
|
||||||
|
if (closing) { shutdown(null); return; }
|
||||||
|
status = STATUS_OPEN;
|
||||||
|
|
||||||
|
Thread writer = new Thread(new Runnable() {
|
||||||
|
@Override public void run() { pumpOut(); }
|
||||||
|
}, "tls-write-" + handle);
|
||||||
|
writer.setDaemon(true);
|
||||||
|
writer.start();
|
||||||
|
|
||||||
|
pumpIn();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
shutdown(describe(t));
|
||||||
|
if (plain != null) {
|
||||||
|
try { plain.close(); } catch (Throwable ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* minSdk is 16, where TLS 1.1/1.2 exist but are off by default. Every
|
||||||
|
* modern server refuses everything older, so switch on whatever the
|
||||||
|
* platform has rather than leaving an old device negotiating TLS 1.0.
|
||||||
|
*/
|
||||||
|
private static void enableModernProtocols(SSLSocket ssl) {
|
||||||
|
try {
|
||||||
|
List<String> wanted = new ArrayList<String>(3);
|
||||||
|
for (String supported : ssl.getSupportedProtocols()) {
|
||||||
|
if (supported.startsWith("TLSv1.1")
|
||||||
|
|| supported.startsWith("TLSv1.2")
|
||||||
|
|| supported.startsWith("TLSv1.3")) {
|
||||||
|
wanted.add(supported);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!wanted.isEmpty()) {
|
||||||
|
ssl.setEnabledProtocols(wanted.toArray(new String[wanted.size()]));
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pumpIn() {
|
||||||
|
try {
|
||||||
|
InputStream in = socket.getInputStream();
|
||||||
|
byte[] buf = new byte[READ_CHUNK];
|
||||||
|
while (!closing) {
|
||||||
|
int n = in.read(buf);
|
||||||
|
if (n < 0) break;
|
||||||
|
if (n == 0) continue;
|
||||||
|
byte[] chunk = new byte[n];
|
||||||
|
System.arraycopy(buf, 0, chunk, 0, n);
|
||||||
|
synchronized (inLock) {
|
||||||
|
if (inAvailable + n > MAX_BUFFERED) {
|
||||||
|
throw new java.io.IOException("read buffer overflow");
|
||||||
|
}
|
||||||
|
inChunks.add(chunk);
|
||||||
|
inAvailable += n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shutdown(null);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
shutdown(describe(t));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pumpOut() {
|
||||||
|
try {
|
||||||
|
OutputStream out = socket.getOutputStream();
|
||||||
|
while (true) {
|
||||||
|
byte[] chunk;
|
||||||
|
synchronized (outLock) {
|
||||||
|
while (outChunks.isEmpty() && !closing && status != STATUS_CLOSED) {
|
||||||
|
outLock.wait();
|
||||||
|
}
|
||||||
|
if (closing || status == STATUS_CLOSED) return;
|
||||||
|
chunk = outChunks.poll();
|
||||||
|
}
|
||||||
|
if (chunk != null) {
|
||||||
|
out.write(chunk);
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
shutdown(describe(t));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] take(int max) {
|
||||||
|
synchronized (inLock) {
|
||||||
|
if (inAvailable <= 0) return null;
|
||||||
|
int want = Math.min(max, inAvailable);
|
||||||
|
byte[] out = new byte[want];
|
||||||
|
int filled = 0;
|
||||||
|
while (filled < want) {
|
||||||
|
byte[] head = inChunks.peek();
|
||||||
|
if (head == null) break;
|
||||||
|
int have = head.length - inHeadOffset;
|
||||||
|
int take = Math.min(have, want - filled);
|
||||||
|
System.arraycopy(head, inHeadOffset, out, filled, take);
|
||||||
|
filled += take;
|
||||||
|
inHeadOffset += take;
|
||||||
|
if (inHeadOffset >= head.length) {
|
||||||
|
inChunks.poll();
|
||||||
|
inHeadOffset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inAvailable -= filled;
|
||||||
|
if (filled == want) return out;
|
||||||
|
byte[] short_ = new byte[filled];
|
||||||
|
System.arraycopy(out, 0, short_, 0, filled);
|
||||||
|
return short_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The handle stays registered until the caller closes it, so why the
|
||||||
|
* connection ended and whatever arrived before it did are both still
|
||||||
|
* readable. Dropping it here instead would turn a server that states its
|
||||||
|
* refusal and hangs up into an unknown handle, which is the one failure a
|
||||||
|
* player most needs the reason for.
|
||||||
|
*/
|
||||||
|
private void shutdown(String why) {
|
||||||
|
if (why != null && error == null) error = why;
|
||||||
|
closing = true;
|
||||||
|
status = STATUS_CLOSED;
|
||||||
|
synchronized (outLock) { outLock.notifyAll(); }
|
||||||
|
SSLSocket s = socket;
|
||||||
|
socket = null;
|
||||||
|
if (s != null) {
|
||||||
|
try { s.close(); } catch (Throwable ignored) {}
|
||||||
|
}
|
||||||
|
if (why != null) Log.d("TlsSocket", host + ":" + port + " -- " + why);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String describe(Throwable t) {
|
||||||
|
String msg = t.getMessage();
|
||||||
|
String name = t.getClass().getSimpleName();
|
||||||
|
if (msg == null || msg.length() == 0) return name;
|
||||||
|
return name + ": " + msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<!--
|
||||||
|
Desktop TLS dialer for mods that need outbound WSS/TLS (e.g. multiworld
|
||||||
|
clients). Native AOT so the game gets a plain C ABI DLL with no .NET
|
||||||
|
runtime to install. Windows uses Schannel via SslStream; Linux and macOS
|
||||||
|
use their platform backends the same way.
|
||||||
|
|
||||||
|
Publish examples:
|
||||||
|
dotnet publish -c Release -r win-x64 -o ../../dist/native/win-x64
|
||||||
|
dotnet publish -c Release -r linux-x64 -o ../../dist/native/linux-x64
|
||||||
|
dotnet publish -c Release -r osx-x64 -o ../../dist/native/osx-x64
|
||||||
|
-->
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<PublishAot>true</PublishAot>
|
||||||
|
<NativeLib>Shared</NativeLib>
|
||||||
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
<StripSymbols>true</StripSymbols>
|
||||||
|
<AssemblyName>gen1tls</AssemblyName>
|
||||||
|
<RootNamespace>Gen1Tls</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# gen1tls — desktop TLS dialer
|
||||||
|
|
||||||
|
Native AOT library that gives mods a non-blocking TLS client with the same
|
||||||
|
handle/poll contract as the Android `TlsSocket` / `love.system.tls*` bridge.
|
||||||
|
|
||||||
|
- **Windows:** Schannel via `SslStream` (system trust store, SNI)
|
||||||
|
- **Linux / macOS:** same project, publish with `-r linux-x64` / `osx-x64` /
|
||||||
|
`osx-arm64` when those builds are wired up
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet publish native/tls_dial/Gen1Tls.csproj -c Release -r win-x64 -o dist/native/win-x64
|
||||||
|
```
|
||||||
|
|
||||||
|
The Windows game zip script already does this:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts/build_windows.ps1 -Version 0.1.77
|
||||||
|
```
|
||||||
|
|
||||||
|
Ship `gen1tls.dll` (or `libgen1tls.so` / `libgen1tls.dylib`) **next to** the
|
||||||
|
fused executable. Mods load it through LuaJIT FFI; no .NET runtime is
|
||||||
|
required on the player's machine.
|
||||||
|
|
||||||
|
## Android
|
||||||
|
|
||||||
|
On Android, the matching API is exposed as `love.system.tlsOpen` /
|
||||||
|
`tlsStatus` / `tlsSend` / `tlsReceive` / `tlsError` / `tlsClose`, backed by
|
||||||
|
`org.love2d.android.TlsSocket`.
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Security;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Security.Authentication;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Gen1Tls;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Non-blocking TLS client with the same handle/poll contract as the Android
|
||||||
|
/// TlsSocket bridge. Connect and handshake run on a background thread; send
|
||||||
|
/// queues until the stream is ready; receive drains a chunk queue. Certificate
|
||||||
|
/// validation and SNI are SslStream's defaults (platform trust store).
|
||||||
|
/// </summary>
|
||||||
|
public static class TlsDialer
|
||||||
|
{
|
||||||
|
public const int StatusConnecting = 0;
|
||||||
|
public const int StatusOpen = 1;
|
||||||
|
public const int StatusClosed = 2;
|
||||||
|
|
||||||
|
const int ConnectTimeoutMs = 15000;
|
||||||
|
const int ReadChunk = 16384;
|
||||||
|
const int MaxBuffered = 4 * 1024 * 1024;
|
||||||
|
|
||||||
|
static readonly ConcurrentDictionary<int, Conn> Live = new();
|
||||||
|
static int NextHandle = 1;
|
||||||
|
|
||||||
|
sealed class Conn
|
||||||
|
{
|
||||||
|
public required string Host;
|
||||||
|
public required int Port;
|
||||||
|
public int Status = StatusConnecting;
|
||||||
|
public string? Error;
|
||||||
|
public bool Closing;
|
||||||
|
public SslStream? Stream;
|
||||||
|
public TcpClient? Client;
|
||||||
|
|
||||||
|
public readonly object InLock = new();
|
||||||
|
public readonly Queue<byte[]> InChunks = new();
|
||||||
|
public int InHeadOffset;
|
||||||
|
public int InAvailable;
|
||||||
|
|
||||||
|
public readonly object OutLock = new();
|
||||||
|
public readonly Queue<byte[]> OutChunks = new();
|
||||||
|
public readonly ManualResetEventSlim OutPulse = new(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- C ABI
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "gen1tls_open")]
|
||||||
|
public static int Open(nint hostPtr, int port)
|
||||||
|
{
|
||||||
|
if (hostPtr == 0 || port <= 0 || port > 65535) return -1;
|
||||||
|
string? host = Marshal.PtrToStringUTF8(hostPtr);
|
||||||
|
if (string.IsNullOrEmpty(host)) return -1;
|
||||||
|
|
||||||
|
int handle = Interlocked.Increment(ref NextHandle);
|
||||||
|
var conn = new Conn { Host = host, Port = port };
|
||||||
|
if (!Live.TryAdd(handle, conn)) return -1;
|
||||||
|
|
||||||
|
var dialer = new Thread(() => Dial(conn))
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "gen1tls-dial-" + handle,
|
||||||
|
};
|
||||||
|
dialer.Start();
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "gen1tls_status")]
|
||||||
|
public static int Status(int handle)
|
||||||
|
=> Live.TryGetValue(handle, out var conn) ? conn.Status : -1;
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "gen1tls_send")]
|
||||||
|
public static int Send(int handle, nint dataPtr, int length)
|
||||||
|
{
|
||||||
|
if (!Live.TryGetValue(handle, out var conn) || dataPtr == 0) return -1;
|
||||||
|
if (conn.Status == StatusClosed) return -1;
|
||||||
|
if (length <= 0) return 0;
|
||||||
|
|
||||||
|
var copy = new byte[length];
|
||||||
|
Marshal.Copy(dataPtr, copy, 0, length);
|
||||||
|
lock (conn.OutLock)
|
||||||
|
{
|
||||||
|
conn.OutChunks.Enqueue(copy);
|
||||||
|
conn.OutPulse.Set();
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "gen1tls_receive")]
|
||||||
|
public static int Receive(int handle, nint bufPtr, int max)
|
||||||
|
{
|
||||||
|
if (!Live.TryGetValue(handle, out var conn) || bufPtr == 0 || max <= 0)
|
||||||
|
return 0;
|
||||||
|
return Take(conn, bufPtr, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "gen1tls_error")]
|
||||||
|
public static int Error(int handle, nint bufPtr, int max)
|
||||||
|
{
|
||||||
|
if (!Live.TryGetValue(handle, out var conn)) return 0;
|
||||||
|
string? err = conn.Error;
|
||||||
|
if (string.IsNullOrEmpty(err) || bufPtr == 0 || max <= 1) return 0;
|
||||||
|
|
||||||
|
byte[] utf8 = Encoding.UTF8.GetBytes(err);
|
||||||
|
int n = Math.Min(utf8.Length, max - 1);
|
||||||
|
Marshal.Copy(utf8, 0, bufPtr, n);
|
||||||
|
Marshal.WriteByte(bufPtr, n, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "gen1tls_close")]
|
||||||
|
public static void Close(int handle)
|
||||||
|
{
|
||||||
|
if (Live.TryRemove(handle, out var conn))
|
||||||
|
Shutdown(conn, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ internals
|
||||||
|
|
||||||
|
static void Dial(Conn conn)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = new TcpClient();
|
||||||
|
var connect = client.ConnectAsync(conn.Host, conn.Port);
|
||||||
|
if (!connect.Wait(ConnectTimeoutMs))
|
||||||
|
throw new TimeoutException($"connect to {conn.Host}:{conn.Port} timed out");
|
||||||
|
connect.GetAwaiter().GetResult();
|
||||||
|
client.NoDelay = true;
|
||||||
|
conn.Client = client;
|
||||||
|
|
||||||
|
var ssl = new SslStream(client.GetStream(), leaveInnerStreamOpen: false);
|
||||||
|
// AuthenticateAsClient sets SNI from targetHost and validates against
|
||||||
|
// the platform trust store -- the whole reason this dialer exists.
|
||||||
|
var auth = ssl.AuthenticateAsClientAsync(conn.Host);
|
||||||
|
if (!auth.Wait(ConnectTimeoutMs))
|
||||||
|
throw new TimeoutException($"TLS handshake with {conn.Host} timed out");
|
||||||
|
auth.GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
conn.Stream = ssl;
|
||||||
|
if (conn.Closing) { Shutdown(conn, null); return; }
|
||||||
|
conn.Status = StatusOpen;
|
||||||
|
|
||||||
|
var writer = new Thread(() => PumpOut(conn))
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "gen1tls-write",
|
||||||
|
};
|
||||||
|
writer.Start();
|
||||||
|
PumpIn(conn);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Shutdown(conn, Describe(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PumpIn(Conn conn)
|
||||||
|
{
|
||||||
|
var stream = conn.Stream;
|
||||||
|
if (stream == null) return;
|
||||||
|
var buf = new byte[ReadChunk];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!conn.Closing)
|
||||||
|
{
|
||||||
|
int n = stream.Read(buf, 0, buf.Length);
|
||||||
|
if (n <= 0) break;
|
||||||
|
var chunk = new byte[n];
|
||||||
|
Buffer.BlockCopy(buf, 0, chunk, 0, n);
|
||||||
|
lock (conn.InLock)
|
||||||
|
{
|
||||||
|
// A stalled Lua pump must not grow forever; drop the
|
||||||
|
// connection rather than the room's backlog.
|
||||||
|
if (conn.InAvailable + n > MaxBuffered)
|
||||||
|
throw new InvalidOperationException("TLS receive buffer overflow");
|
||||||
|
conn.InChunks.Enqueue(chunk);
|
||||||
|
conn.InAvailable += n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!conn.Closing) Shutdown(conn, Describe(ex));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Shutdown(conn, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PumpOut(Conn conn)
|
||||||
|
{
|
||||||
|
var stream = conn.Stream;
|
||||||
|
if (stream == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!conn.Closing)
|
||||||
|
{
|
||||||
|
byte[]? chunk = null;
|
||||||
|
lock (conn.OutLock)
|
||||||
|
{
|
||||||
|
if (conn.OutChunks.Count == 0)
|
||||||
|
{
|
||||||
|
conn.OutPulse.Reset();
|
||||||
|
// fall through to wait outside the lock
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
chunk = conn.OutChunks.Dequeue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chunk == null)
|
||||||
|
{
|
||||||
|
conn.OutPulse.Wait(250);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
stream.Write(chunk, 0, chunk.Length);
|
||||||
|
stream.Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!conn.Closing) Shutdown(conn, Describe(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Take(Conn conn, nint bufPtr, int max)
|
||||||
|
{
|
||||||
|
lock (conn.InLock)
|
||||||
|
{
|
||||||
|
if (conn.InAvailable == 0 || conn.InChunks.Count == 0) return 0;
|
||||||
|
int copied = 0;
|
||||||
|
while (copied < max && conn.InChunks.Count > 0)
|
||||||
|
{
|
||||||
|
byte[] head = conn.InChunks.Peek();
|
||||||
|
int avail = head.Length - conn.InHeadOffset;
|
||||||
|
int n = Math.Min(avail, max - copied);
|
||||||
|
Marshal.Copy(head, conn.InHeadOffset, bufPtr + copied, n);
|
||||||
|
copied += n;
|
||||||
|
conn.InHeadOffset += n;
|
||||||
|
conn.InAvailable -= n;
|
||||||
|
if (conn.InHeadOffset >= head.Length)
|
||||||
|
{
|
||||||
|
conn.InChunks.Dequeue();
|
||||||
|
conn.InHeadOffset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return copied;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Shutdown(Conn conn, string? why)
|
||||||
|
{
|
||||||
|
if (conn.Closing && why == null && conn.Status == StatusClosed) return;
|
||||||
|
conn.Closing = true;
|
||||||
|
if (why != null) conn.Error = why;
|
||||||
|
conn.Status = StatusClosed;
|
||||||
|
conn.OutPulse.Set();
|
||||||
|
try { conn.Stream?.Dispose(); } catch { /* ignore */ }
|
||||||
|
try { conn.Client?.Dispose(); } catch { /* ignore */ }
|
||||||
|
conn.Stream = null;
|
||||||
|
conn.Client = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Describe(Exception ex)
|
||||||
|
{
|
||||||
|
for (Exception? e = ex; e != null; e = e.InnerException)
|
||||||
|
{
|
||||||
|
if (e is AuthenticationException) return e.Message;
|
||||||
|
if (e is SocketException se) return se.Message;
|
||||||
|
if (e is TimeoutException) return e.Message;
|
||||||
|
if (e is IOException) return e.Message;
|
||||||
|
}
|
||||||
|
return ex.GetBaseException().Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Build a Windows win64 Gen1Recomp zip with native TLS (gen1tls.dll).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# powershell -ExecutionPolicy Bypass -File scripts\build_windows.ps1 [-Version 0.1.77]
|
||||||
|
#
|
||||||
|
# Produces:
|
||||||
|
# dist\win\gen1recomp-<version>-windows.zip
|
||||||
|
# gen1recomp.exe fused LÖVE + game.love
|
||||||
|
# gen1tls.dll Native AOT TLS dialer (Schannel via SslStream)
|
||||||
|
#
|
||||||
|
# Requires: .NET 8 SDK (for Native AOT), Git Bash, and tools/winbuild on PATH
|
||||||
|
# for pack_love.sh under Git Bash on Windows.
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$Version = "0.1.77"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$Root = Split-Path -Parent $PSScriptRoot
|
||||||
|
if (-not (Test-Path (Join-Path $Root "main.lua"))) {
|
||||||
|
$Root = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Version -notmatch '^\d+\.\d+\.\d+$') {
|
||||||
|
throw "Version must be X.Y.Z (got '$Version')"
|
||||||
|
}
|
||||||
|
|
||||||
|
$Cache = Join-Path $Root ".bazinga\cache"
|
||||||
|
$Work = Join-Path $Root ".bazinga\work"
|
||||||
|
$OutDir = Join-Path $Work "gen1recomp-win64"
|
||||||
|
$DistDir = Join-Path $Root "dist\win"
|
||||||
|
$NativeOut = Join-Path $Root "dist\native\win-x64"
|
||||||
|
$LoveZip = Join-Path $Cache "love-11.5-win64.zip"
|
||||||
|
$LoveUrl = "https://github.com/love2d/love/releases/download/11.5/love-11.5-win64.zip"
|
||||||
|
$Bash = "C:\Program Files\Git\bin\bash.exe"
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path $Cache, $Work, $DistDir, $NativeOut | Out-Null
|
||||||
|
|
||||||
|
# Put zip/python shims first so pack_love.sh works in Git Bash on Windows.
|
||||||
|
$env:PATH = "$(Join-Path $Root 'tools\winbuild');$env:PATH"
|
||||||
|
|
||||||
|
Write-Host "==> building gen1tls.dll (Native AOT)"
|
||||||
|
dotnet publish (Join-Path $Root "native\tls_dial\Gen1Tls.csproj") `
|
||||||
|
-c Release -r win-x64 -o $NativeOut
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "gen1tls publish failed" }
|
||||||
|
$TlsDll = Join-Path $NativeOut "gen1tls.dll"
|
||||||
|
if (-not (Test-Path $TlsDll)) { throw "gen1tls.dll missing after publish" }
|
||||||
|
|
||||||
|
if (-not (Test-Path $LoveZip) -or (Get-Item $LoveZip).Length -lt 1MB) {
|
||||||
|
Write-Host "==> downloading love-11.5-win64.zip"
|
||||||
|
Invoke-WebRequest -Uri $LoveUrl -OutFile $LoveZip -UseBasicParsing
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "==> packing game.love (engine $Version)"
|
||||||
|
if (-not (Test-Path $Bash)) { throw "Git Bash not found at $Bash" }
|
||||||
|
$RootPosix = ($Root -replace '\\', '/') -replace '^([A-Za-z]):', '/$1'
|
||||||
|
& $Bash -lc "cd '$RootPosix' && PATH=`"`$PWD/tools/winbuild:`$PATH`" scripts/pack_love.sh --output .bazinga/work/game.love --listing .bazinga/work/love-listing.txt --version $Version"
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "pack_love failed" }
|
||||||
|
|
||||||
|
Write-Host "==> fusing gen1recomp.exe"
|
||||||
|
$Extract = Join-Path $Work "love-win64"
|
||||||
|
Remove-Item -Recurse -Force $Extract, $OutDir -ErrorAction SilentlyContinue
|
||||||
|
New-Item -ItemType Directory -Force -Path $Extract, $OutDir | Out-Null
|
||||||
|
Expand-Archive -Path $LoveZip -DestinationPath $Extract -Force
|
||||||
|
$LoveDir = Get-ChildItem $Extract -Directory | Select-Object -First 1
|
||||||
|
Copy-Item (Join-Path $LoveDir.FullName "*.dll") $OutDir
|
||||||
|
Copy-Item (Join-Path $LoveDir.FullName "license.txt") $OutDir -ErrorAction SilentlyContinue
|
||||||
|
Copy-Item $TlsDll $OutDir
|
||||||
|
|
||||||
|
$out = [IO.File]::Create((Join-Path $OutDir "gen1recomp.exe"))
|
||||||
|
try {
|
||||||
|
$a = [IO.File]::OpenRead((Join-Path $LoveDir.FullName "love.exe"))
|
||||||
|
try { $a.CopyTo($out) } finally { $a.Dispose() }
|
||||||
|
$b = [IO.File]::OpenRead((Join-Path $Work "game.love"))
|
||||||
|
try { $b.CopyTo($out) } finally { $b.Dispose() }
|
||||||
|
} finally { $out.Dispose() }
|
||||||
|
|
||||||
|
@"
|
||||||
|
Gen1Recomp Windows (engine $Version) — native TLS
|
||||||
|
|
||||||
|
gen1tls.dll sits next to gen1recomp.exe and exposes a non-blocking TLS
|
||||||
|
client (Windows Schannel via .NET SslStream, Native AOT). Mods can load it
|
||||||
|
through LuaJIT FFI, or call love.system.tls* on Android.
|
||||||
|
|
||||||
|
Keep gen1tls.dll beside the executable when you redistribute the zip.
|
||||||
|
"@ | Set-Content (Join-Path $OutDir "README-TLS.txt") -Encoding UTF8
|
||||||
|
|
||||||
|
$ZipOut = Join-Path $DistDir "gen1recomp-$Version-windows.zip"
|
||||||
|
Remove-Item $ZipOut -ErrorAction SilentlyContinue
|
||||||
|
Compress-Archive -Path $OutDir -DestinationPath $ZipOut -Force
|
||||||
|
|
||||||
|
Write-Host "==> built $ZipOut"
|
||||||
|
Get-Item $ZipOut | ForEach-Object { " {0:N1} MB" -f ($_.Length / 1MB) }
|
||||||
|
Get-ChildItem $OutDir | ForEach-Object { " $($_.Name)" }
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Windows ships a python3.exe under WindowsApps that exists purely to advertise
|
||||||
|
# the Microsoft Store, so `command -v python3` succeeds and running it fails.
|
||||||
|
# build_android.sh calls python3 three times (the Yellow manifest check, the
|
||||||
|
# gradle.properties rewrite and the manifest permission trim), and this puts a
|
||||||
|
# real interpreter behind that name for the length of the build.
|
||||||
|
exec py "$@"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Stands in for Info-ZIP's `zip`, which Git for Windows does not ship. Put
|
||||||
|
# this directory first on PATH before running scripts/build_android.sh; see
|
||||||
|
# zip_impl.py for what is and is not supported.
|
||||||
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
exec py "$here/zip_impl.py" "$@"
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
"""Enough of Info-ZIP's `zip` for scripts/build_android.sh, on Windows.
|
||||||
|
|
||||||
|
Git for Windows ships unzip but not zip, which is the one tool standing between
|
||||||
|
a Windows checkout and a local Android build. Rather than fetching a binary
|
||||||
|
from somewhere unaudited, this covers exactly the two forms the build script
|
||||||
|
uses and refuses anything it does not understand, so a future flag fails loudly
|
||||||
|
instead of silently producing a wrong archive:
|
||||||
|
|
||||||
|
zip -q -9 -r out.love main.lua src data -x '*.DS_Store' -x 'data/generated/*'
|
||||||
|
zip -q out.love src/core/Version.lua # add or replace one entry
|
||||||
|
|
||||||
|
Paths are stored relative to the working directory with forward slashes, and
|
||||||
|
directories are walked in sorted order so the archive is reproducible.
|
||||||
|
"""
|
||||||
|
import fnmatch
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
|
def excluded(name, patterns):
|
||||||
|
# zip's wildcards cross directory separators, which is also what fnmatch
|
||||||
|
# does, so the build script's '*/.git/*' behaves the same either way.
|
||||||
|
return any(fnmatch.fnmatch(name, pattern) for pattern in patterns)
|
||||||
|
|
||||||
|
|
||||||
|
def collect(paths, patterns):
|
||||||
|
"""-> archive-relative names, in a stable order."""
|
||||||
|
names = []
|
||||||
|
for path in paths:
|
||||||
|
if os.path.isdir(path):
|
||||||
|
for root, dirs, files in os.walk(path):
|
||||||
|
dirs.sort()
|
||||||
|
for f in sorted(files):
|
||||||
|
full = os.path.join(root, f)
|
||||||
|
name = os.path.relpath(full, ".").replace(os.sep, "/")
|
||||||
|
if not excluded(name, patterns):
|
||||||
|
names.append(name)
|
||||||
|
elif os.path.isfile(path):
|
||||||
|
name = os.path.relpath(path, ".").replace(os.sep, "/")
|
||||||
|
if not excluded(name, patterns):
|
||||||
|
names.append(name)
|
||||||
|
else:
|
||||||
|
sys.stderr.write("zip: %s not found\n" % path)
|
||||||
|
return None
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
patterns, paths, archive = [], [], None
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
arg = argv[i]
|
||||||
|
if arg == "-x":
|
||||||
|
# Info-ZIP accepts one -x followed by several patterns, which is how
|
||||||
|
# pack_love.sh and build.sh write their excludes. Consume every
|
||||||
|
# following non-flag argument as a pattern; paths always come before
|
||||||
|
# -x in those scripts, so this does not steal archive members.
|
||||||
|
i += 1
|
||||||
|
if i >= len(argv) or argv[i].startswith("-"):
|
||||||
|
sys.stderr.write("zip: -x needs a pattern\n")
|
||||||
|
return 2
|
||||||
|
while i < len(argv) and not argv[i].startswith("-"):
|
||||||
|
patterns.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
elif arg in ("-q", "-9", "-r", "-X", "-o"):
|
||||||
|
pass # quiet, compression level, recurse, no-extra, ordering
|
||||||
|
elif arg.startswith("-"):
|
||||||
|
sys.stderr.write("zip shim: unsupported flag %s\n" % arg)
|
||||||
|
return 2
|
||||||
|
elif archive is None:
|
||||||
|
archive = arg
|
||||||
|
else:
|
||||||
|
paths.append(arg)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if archive is None or not paths:
|
||||||
|
sys.stderr.write("usage: zip [-q9r] [-x pat] archive path...\n")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
names = collect(paths, patterns)
|
||||||
|
if names is None:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Adding to an existing archive means rewriting it: zipfile can append, but
|
||||||
|
# appending a name that is already in there leaves both copies and readers
|
||||||
|
# disagree about which one wins. The version stamp does exactly that.
|
||||||
|
keep = []
|
||||||
|
if os.path.exists(archive):
|
||||||
|
replacing = set(names)
|
||||||
|
with zipfile.ZipFile(archive, "r") as old:
|
||||||
|
for info in old.infolist():
|
||||||
|
if info.filename not in replacing:
|
||||||
|
keep.append((info, old.read(info.filename)))
|
||||||
|
|
||||||
|
tmp = archive + ".shimtmp"
|
||||||
|
with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as out:
|
||||||
|
for info, data in keep:
|
||||||
|
out.writestr(info, data)
|
||||||
|
for name in names:
|
||||||
|
out.write(name, name)
|
||||||
|
|
||||||
|
if os.path.exists(archive):
|
||||||
|
os.remove(archive)
|
||||||
|
os.rename(tmp, archive)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
Reference in New Issue
Block a user