mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
Merge pull request #1064 from TheRealSolidusSnake/feature/tls-support
TLS support
This commit is contained in:
@@ -68,3 +68,10 @@ mobile/ios/bundle_id.local
|
||||
/ports/uwp/build/
|
||||
/ports/uwp/third_party/*/source/
|
||||
/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
|
||||
|
||||
#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 }
|
||||
};
|
||||
|
||||
@@ -606,6 +606,45 @@ public class GameActivity extends SDLActivity {
|
||||
* 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.
|
||||
*/
|
||||
/**
|
||||
* 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
|
||||
public static boolean httpDownload(String url, String destPath, String userAgent, String accept) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,90 @@
|
||||
"tintColor": "3b5ca8",
|
||||
"category": "games",
|
||||
"versions": [
|
||||
{
|
||||
"version": "0.1.76",
|
||||
"date": "2026-08-10",
|
||||
"size": 9610081,
|
||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.76/gen1recomp-0.1.76-ios.ipa",
|
||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #617 Old Man in Viridian\n- #889 Pokemon yellow can't export save file right\n- #915 wrong sprite used in opening\n- #916 Trainer sprite at end of fly animation\n- #931 Mod update checks fail\n- #932 Bugs reset settings\n- #933 Add TitleState override for player\n- #945 Cannot edit Trainer Class's Battle Theme\n- #969 Linux x86_64 AppImage Shows up as \"LOVE\" instead of \"gen1recomp\"\n- #1016 Mod API: support variable-size overworld sprites\n- #1039 Encounter rate grace period not working\n- #1040 Player sprite walks right through rival after defeating him at the end of the game\n\n## Contributors\n\n- @ArmstrongThomas\n- @AverageConsumer\n- @Bortlesboat\n- @bryanthaboi\n- @crusty\n- @dlloa\n- @jherediagu\n- @KikiManjaro\n- @martin2844\n- @MaxTomahawk\n- @ShaneMcGovernIE\n- @steve1337\n- @swuff-star\n- @thibautbus\n- @Yukitty\n- hernan"
|
||||
},
|
||||
{
|
||||
"version": "0.1.75",
|
||||
"date": "2026-08-06",
|
||||
"size": 9574086,
|
||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.75/gen1recomp-0.1.75-ios.ipa",
|
||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #826 \"Super effective\" and \"not very effective\" SFX are reversed\n- #848 Roms not showing up\n- #876 Remote mod download is unavailable on this platform\n- #899 Importing Pokémon red leads to the contents extracted without a folder called 'red'\n- #902 Audio is wrong!\n\n## Contributors\n\n- @andrewqsantos\n- @bryanthaboi\n- @caorthann-celt\n- @johnjohto\n- @ShaneMcGovernIE"
|
||||
},
|
||||
{
|
||||
"version": "0.1.74",
|
||||
"date": "2026-08-06",
|
||||
"size": 9588946,
|
||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.74/gen1recomp-0.1.74-ios.ipa",
|
||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #883 Evolution Stones consumed if evolution is cancelled\n- #887 MacOS Closing game just keeps reopening\n- #894 Max Repel usable in battle\n\n## Contributors\n\n- @bryanthaboi\n- @MarceloMachadoxD\n- @ratherDashing"
|
||||
},
|
||||
{
|
||||
"version": "0.1.73",
|
||||
"date": "2026-08-06",
|
||||
"size": 9585729,
|
||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.73/gen1recomp-0.1.73-ios.ipa",
|
||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #878 Mod API: allow active screen states to be hidden from the main render\n\n## Contributors\n\n- @AverageConsumer\n- @bryanthaboi"
|
||||
},
|
||||
{
|
||||
"version": "0.1.72",
|
||||
"date": "2026-08-05",
|
||||
"size": 9586678,
|
||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.72/gen1recomp-0.1.72-ios.ipa",
|
||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #604 Android, retroid pocket 2+ Rom won't import\n- #666 Pikachu emotions are not working on Android\n- #716 Lock Auto Rotate Mobile\n- #727 [Bug] [Windows] Gen1 Recomp \"still in use\" after closing\n- #763 Some EVENTS are turned off\n- #781 Mouse cursor broken on Linux with multi-monitor X11 setup\n- #784 Leech Seed effect\n- #799 Held direction randomly stops player movement (requires re-input)\n- #801 Cannot update mods from the launcher (MacOS)\n- #810 Launcher menu cuts off in vertical mode iOS\n- #828 Closing the app causes settings in launcher to reset\n- #834 Mod import failing\n- #838 Exporting save file Pokemon Yellow\n- #839 AYN Thor Misplaced Data files\n- #849 Public folder support on iOS\n- #852 Cannot switch between saves states on smaller 4:3 screen or in vertical mode\n- #857 Mt. Moon Fossils Reappeared and Won’t Disappear.\n- #863 [Yellow] When you use stairs, Pikachu shouldn't be next to you in the new area\n- #864 Faithful Ratio\n- #867 Missing Dialogue after defeating Marowak in Pokemon Tower\n- #869 Giovanni moves up to the player too early\n- #870 Start Menu on Classic Color\n- #872 Missing text when finding an item with full inventory\n\n## Contributors\n\n- @bryanthaboi"
|
||||
},
|
||||
{
|
||||
"version": "0.1.71",
|
||||
"date": "2026-08-05",
|
||||
"size": 9569738,
|
||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.71/gen1recomp-0.1.71-ios.ipa",
|
||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #806 Haptic Feedback for On-screen controls (mobile)\n- #809 NPC Stuck in Rock when battle triggered and pushing rock towards it\n- #853 When selecting one of the two pokemon after defeating the Karate Master it should show the Pokedex entry\n- #854 Textbox disappears when the yes/no dialogue appears\n- #860 Disabled moves can still be used in the turn they were disabled\n- #862 Casino Poster Rocket Grunt walks into the poster and doesn't \"Dang!\"\n- #865 [Yellow] James doesn't move in multiple encounters\n- #866 [Yellow] Dialogue in wrong \"order\" after multiple encounters (J+J, Giovanni, probably more)\n\n## Contributors\n\n- @bryanthaboi"
|
||||
},
|
||||
{
|
||||
"version": "0.1.70",
|
||||
"date": "2026-08-05",
|
||||
"size": 9562731,
|
||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.70/gen1recomp-0.1.70-ios.ipa",
|
||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #590 Launcher/Save Editor/Dex\n- #788 Fly to the Pokemon Centers in the Routes is not available in original\n- #795 Fly logic is drastically different from the originals.\n- #796 Using Rare Candy from the menu closes it out.\n- #797 Gym Leaders giving items when your bag is full / Bypassing bag limit.\n- #805 Escape Rope Moltres Tower\n- #826 \"Super effective\" and \"not very effective\" SFX are reversed\n- #833 Cancelling nickname entry results in \"A\" as the nickname\n- #835 Restarting the launcher forgets the last rom used\n- #837 Wrong sound effect for Pikachu when entering battle\n- #844 Blizzard sound effect.\n- #845 Moderate issue: Fuchsia City binoculars.\n- #846 Surfing speed after using the bicycle.\n- #847 Minor issues related to the endgame.\n\n## Contributors\n\n- @bryanthaboi\n- @dburton95\n- @johnjohto\n- @KikiManjaro"
|
||||
},
|
||||
{
|
||||
"version": "0.1.69",
|
||||
"date": "2026-08-04",
|
||||
"size": 9552851,
|
||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.69/gen1recomp-0.1.69-ios.ipa",
|
||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #768 Menu behaviour for Pokemon and HM moves\n- #785 Back to launcher\n- #792 HM moves in the wrong position in the menu.\n- #807 Expose gameplay pointer events and source-safe mod input injection\n- #811 Untranslatables\n- #814 [minor thing] bold arrow on move swap (select)\n\n## Contributors\n\n- @bryanthaboi\n- @johnjohto"
|
||||
},
|
||||
{
|
||||
"version": "0.1.68",
|
||||
"date": "2026-08-04",
|
||||
"size": 9768011,
|
||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.68/gen1recomp-0.1.68-ios.ipa",
|
||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @bryanthaboi\n- @caorthann-celt"
|
||||
},
|
||||
{
|
||||
"version": "0.1.67",
|
||||
"date": "2026-08-04",
|
||||
"size": 9767619,
|
||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.67/gen1recomp-0.1.67-ios.ipa",
|
||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #373 Lines at Title Screen\n- #644 Substitute failing causes visual issues\n- #673 iOS: thin vertical seam below the START menu panel (v0.1.56, iPhone 15 Pro Max)\n- #703 Credits playing too fast (song ends after it should)\n- #726 Surfing Pikachu Minigame Broken\n- #737 Battle menu move cursor fails to reset to the first slot after switching Pokémon\n- #750 NPC (possibly player) trades still graphically broken\n- #752 Launcher exports save to AppData while in portable mode\n- #764 Trainer Fanfare doesn't play\n- #765 Text Advance broken in certain aspects\n- #768 Menu behaviour for Pokemon and HM moves\n- #773 Battle screen colours messed up when BG = World, battle in un-flashed Rock Tunnel\n- #774 bug(build): Desktop build can reject a valid game archive under pipefail\n- #775 TM42 Dream Eater dialog.\n- #777 Battle Screen is Very dark\n- #780 Do not delete save\n- #782 Giovanni battle at Silph Co plays wrong song\n\n## Contributors\n\n- @bryanthaboi\n- @luisgonzaleznf\n- @ShaneMcGovernIE"
|
||||
},
|
||||
{
|
||||
"version": "0.1.66",
|
||||
"date": "2026-08-04",
|
||||
"size": 9749980,
|
||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.66/gen1recomp-0.1.66-ios.ipa",
|
||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #691 Save file transfer\n- #743 Cannot Scroll Main Menu/Mod Menu\n- #779 \"Enemy \" untranslateable\n\n## Contributors\n\n- @bryanthaboi\n- @jherediagu\n- @ShaneMcGovernIE\n- @vegerot"
|
||||
},
|
||||
{
|
||||
"version": "0.1.65",
|
||||
"date": "2026-08-03",
|
||||
"size": 9376618,
|
||||
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.65/gen1recomp-0.1.65-ios.ipa",
|
||||
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #592 Screen Orientation\n- #702 FLY overworld animation incorrect/incomplete\n- #748 Launcher Menu has overlap\n- #754 Can push boulders with STRENGTH through walls\n- #758 PC to Mac Online Multiplayer Disconnects Shortly After Starting Match\n\n## Contributors\n\n- @andrewqsantos\n- @Bortlesboat\n- @bryanthaboi\n- @castdrian\n- @johnjohto\n- @ShaneMcGovernIE"
|
||||
},
|
||||
{
|
||||
"version": "0.1.64",
|
||||
"date": "2026-08-03",
|
||||
|
||||
@@ -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