mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 11:11:10 +02:00
feat(android): add httpPost bridge for mod.postLog log sends
Android ships no curl and the JNI bridge was GET-only, so mod.postLog failed there with 'no POST transport on this platform' (HostShell.lua). Add the mirror of httpDownload: GameActivity.httpPost (https-only, hand-followed redirects re-POSTing the body, one-way), the JNI bridge with the same old-APK-skew tolerance, the love.system.httpPost binding, and the HostShell arm that rides it when curl is absent. The body crosses the JNI as raw bytes (jbyteArray) so a log ring with arbitrary UTF-8 cannot corrupt through modified-UTF-8 jstring conversion.
This commit is contained in:
@@ -325,6 +325,55 @@ bool httpDownload(const char *url, const char *destPath, const char *userAgent,
|
||||
return result;
|
||||
}
|
||||
|
||||
bool httpPost(const char *url, const char *body, int bodyLen, const char *contentType, const char *userAgent)
|
||||
{
|
||||
if (url == nullptr || body == nullptr || bodyLen < 0)
|
||||
return false;
|
||||
|
||||
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
|
||||
// Same resolution rule as httpDownload: the activity's own class via
|
||||
// SDL_AndroidGetActivity, never FindClass -- this bridge is called off
|
||||
// the main thread (love.thread workers), whose class loader cannot see
|
||||
// app classes.
|
||||
jobject activityObj = (jobject) SDL_AndroidGetActivity();
|
||||
if (activityObj == nullptr)
|
||||
return false;
|
||||
jclass activity = env->GetObjectClass(activityObj);
|
||||
env->DeleteLocalRef(activityObj);
|
||||
|
||||
// Old APK / new liblove skew: report "no transport" the same way a
|
||||
// missing curl does, instead of aborting on a missing method (#597).
|
||||
jmethodID method = env->GetStaticMethodID(activity, "httpPost",
|
||||
"(Ljava/lang/String;[BLjava/lang/String;Ljava/lang/String;)Z");
|
||||
if (method == nullptr)
|
||||
{
|
||||
env->ExceptionClear();
|
||||
env->DeleteLocalRef(activity);
|
||||
return false;
|
||||
}
|
||||
|
||||
jstring jurl = env->NewStringUTF(url);
|
||||
// raw bytes across the bridge: a log ring can carry arbitrary UTF-8,
|
||||
// and a jstring would run it through modified UTF-8
|
||||
jbyteArray jbody = env->NewByteArray(bodyLen);
|
||||
if (jbody != nullptr)
|
||||
env->SetByteArrayRegion(jbody, 0, bodyLen, (const jbyte*) body);
|
||||
jstring jct = contentType != nullptr ? env->NewStringUTF(contentType) : nullptr;
|
||||
jstring jua = userAgent != nullptr ? env->NewStringUTF(userAgent) : nullptr;
|
||||
|
||||
jboolean result = env->CallStaticBooleanMethod(activity, method, jurl, jbody, jct, jua);
|
||||
|
||||
env->DeleteLocalRef(jurl);
|
||||
if (jbody != nullptr)
|
||||
env->DeleteLocalRef(jbody);
|
||||
if (jct != nullptr)
|
||||
env->DeleteLocalRef(jct);
|
||||
if (jua != nullptr)
|
||||
env->DeleteLocalRef(jua);
|
||||
env->DeleteLocalRef(activity);
|
||||
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
|
||||
|
||||
@@ -98,6 +98,14 @@ bool restartApp();
|
||||
**/
|
||||
bool httpDownload(const char *url, const char *destPath, const char *userAgent, const char *accept);
|
||||
|
||||
/**
|
||||
* Blocking HTTPS POST of a raw byte body (GameActivity.httpPost). The
|
||||
* mirror of httpDownload for mod.postLog log sends, which need POST and
|
||||
* have no curl on Android. contentType / userAgent may be null. Returns
|
||||
* whether the server accepted the send (2xx).
|
||||
**/
|
||||
bool httpPost(const char *url, const char *body, int bodyLen, const char *contentType, const char *userAgent);
|
||||
|
||||
/**
|
||||
* TLS client sockets (GameActivity.tls*, implemented by TlsSocket.java).
|
||||
* LuaSocket, which is what LOVE ships, does TCP only, so wss:// is otherwise
|
||||
|
||||
@@ -259,6 +259,21 @@ bool System::httpDownload(const char *url, const char *destPath,
|
||||
#endif
|
||||
}
|
||||
|
||||
bool System::httpPost(const char *url, const char *body, int bodyLen,
|
||||
const char *contentType, const char *userAgent) const
|
||||
{
|
||||
#ifdef LOVE_ANDROID
|
||||
return love::android::httpPost(url, body, bodyLen, contentType, userAgent);
|
||||
#else
|
||||
LOVE_UNUSED(url);
|
||||
LOVE_UNUSED(body);
|
||||
LOVE_UNUSED(bodyLen);
|
||||
LOVE_UNUSED(contentType);
|
||||
LOVE_UNUSED(userAgent);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
int System::tlsOpen(const char *host, int port) const
|
||||
{
|
||||
#ifdef LOVE_ANDROID
|
||||
|
||||
@@ -151,6 +151,14 @@ public:
|
||||
virtual bool httpDownload(const char *url, const char *destPath,
|
||||
const char *userAgent = nullptr, const char *accept = nullptr) const;
|
||||
|
||||
/**
|
||||
* Blocking HTTPS POST of a raw byte body (Android only; false
|
||||
* elsewhere). The mirror of httpDownload for mod.postLog log sends,
|
||||
* which need POST and have no curl on Android (#597).
|
||||
**/
|
||||
virtual bool httpPost(const char *url, const char *body, int bodyLen,
|
||||
const char *contentType = nullptr, const char *userAgent = nullptr) const;
|
||||
|
||||
/**
|
||||
* TLS client sockets (Android only; every call fails elsewhere, where
|
||||
* LuaSec or another provider is the answer). Non-blocking by contract:
|
||||
|
||||
@@ -139,6 +139,17 @@ int w_httpDownload(lua_State *L)
|
||||
return 1;
|
||||
}
|
||||
|
||||
int w_httpPost(lua_State *L)
|
||||
{
|
||||
const char *url = luaL_checkstring(L, 1);
|
||||
size_t bodyLen = 0;
|
||||
const char *body = luaL_checklstring(L, 2, &bodyLen);
|
||||
const char *ct = luaL_optstring(L, 3, nullptr);
|
||||
const char *ua = luaL_optstring(L, 4, nullptr);
|
||||
luax_pushboolean(L, instance()->httpPost(url, body, (int) bodyLen, ct, ua));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int w_hasBackgroundMusic(lua_State *L)
|
||||
{
|
||||
lua_pushboolean(L, instance()->hasBackgroundMusic());
|
||||
@@ -233,6 +244,7 @@ static const luaL_Reg functions[] =
|
||||
{ "syncHealthSteps", w_syncHealthSteps },
|
||||
{ "restartApp", w_restartApp },
|
||||
{ "httpDownload", w_httpDownload },
|
||||
{ "httpPost", w_httpPost },
|
||||
{ "tlsOpen", w_tlsOpen },
|
||||
{ "tlsStatus", w_tlsStatus },
|
||||
{ "tlsSend", w_tlsSend },
|
||||
|
||||
@@ -741,6 +741,76 @@ public class GameActivity extends SDLActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocking HTTPS POST, exposed as love.system.httpPost and used by
|
||||
* src/core/HostShell.lua for mod.postLog. The GET bridge above covers
|
||||
* downloads; log sends need POST, and Android ships no curl, so this is
|
||||
* the only POST transport the platform has. Strictly one-way, matching
|
||||
* the curl branch it mirrors: the response body is drained and
|
||||
* discarded, and only the 2xx verdict comes back.
|
||||
*
|
||||
* Same rules as httpDownload: https only, redirects followed by hand
|
||||
* (re-POSTing the body on each hop, the way curl -X POST behaves), and
|
||||
* the call is blocking on the Lua/worker thread -- never the UI thread.
|
||||
* The body arrives as raw bytes (a jbyteArray across the JNI) because a
|
||||
* log ring can carry arbitrary UTF-8; a String would risk modified-UTF-8
|
||||
* corruption on characters outside the BMP.
|
||||
*/
|
||||
@Keep
|
||||
public static boolean httpPost(String url, byte[] body, String contentType, String userAgent) {
|
||||
if (url == null || body == null) return false;
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
String current = url;
|
||||
for (int hop = 0; hop < 5; hop++) {
|
||||
URL parsed = new URL(current);
|
||||
if (!"https".equalsIgnoreCase(parsed.getProtocol())) return false;
|
||||
conn = (HttpURLConnection) parsed.openConnection();
|
||||
conn.setInstanceFollowRedirects(false);
|
||||
conn.setConnectTimeout(15000);
|
||||
conn.setReadTimeout(60000);
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("User-Agent",
|
||||
userAgent == null ? "gen1recomp" : userAgent);
|
||||
conn.setRequestProperty("Content-Type",
|
||||
contentType == null ? "text/plain" : contentType);
|
||||
OutputStream out = new BufferedOutputStream(conn.getOutputStream());
|
||||
try {
|
||||
out.write(body);
|
||||
} finally {
|
||||
try { out.close(); } catch (IOException ignored) {}
|
||||
}
|
||||
int code = conn.getResponseCode();
|
||||
if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) {
|
||||
String next = conn.getHeaderField("Location");
|
||||
conn.disconnect();
|
||||
conn = null;
|
||||
if (next == null) return false;
|
||||
current = new URL(parsed, next).toString();
|
||||
continue;
|
||||
}
|
||||
if (code < 200 || code > 299) return false;
|
||||
// drain and discard, so a slow server cannot wedge the
|
||||
// worker on a full socket buffer
|
||||
InputStream in = new BufferedInputStream(conn.getInputStream());
|
||||
try {
|
||||
byte[] buf = new byte[16384];
|
||||
while (in.read(buf) > 0) {}
|
||||
} finally {
|
||||
try { in.close(); } catch (IOException ignored) {}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
Log.d("GameActivity", "httpPost failed: " + e.getMessage());
|
||||
return false;
|
||||
} finally {
|
||||
if (conn != null) conn.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows ACTION_CREATE_DOCUMENT so the player can save a staged export
|
||||
* (pending_export.sav in the app save identity) to Downloads / Drive /
|
||||
|
||||
+15
-3
@@ -452,9 +452,11 @@ end
|
||||
-- POST returning success/failure. Strictly one-way: the response body is
|
||||
-- discarded, only the HTTP status class is surfaced (postLog callers never
|
||||
-- trust the reply). curl --data-binary reads the payload from a pipe, so a
|
||||
-- large body never lands in the command line; the Android bridge has no POST
|
||||
-- transport, and httpPost reports that instead of half-working through
|
||||
-- httpDownload (a GET round-trip to a POST endpoint would be a lie).
|
||||
-- large body never lands in the command line; where curl is absent (Android
|
||||
-- and the other bridge-only platforms) the POST rides the JNI bridge --
|
||||
-- love.system.httpPost, the dedicated POST arm added beside httpDownload --
|
||||
-- instead of half-working through httpDownload (a GET round-trip to a POST
|
||||
-- endpoint would be a lie).
|
||||
function HostShell.httpPost(url, body, contentType, userAgent, maxTime)
|
||||
if type(url) ~= "string" or url == "" then return nil, "missing url" end
|
||||
if type(body) ~= "string" then return nil, "missing body" end
|
||||
@@ -530,6 +532,16 @@ function HostShell.httpPost(url, body, contentType, userAgent, maxTime)
|
||||
if not haveBridge() then
|
||||
return nil, "no network transport on this platform"
|
||||
end
|
||||
-- The GET bridge has no POST; the dedicated love.system.httpPost arm
|
||||
-- (GameActivity.httpPost) is the transport where curl is missing. A
|
||||
-- build without it reports the same "no POST transport" a missing curl
|
||||
-- would -- the old-APK skew path in the JNI bridge returns false.
|
||||
if love.system and type(love.system.httpPost) == "function" then
|
||||
local ok, sent = pcall(love.system.httpPost, url, body, contentType,
|
||||
userAgent)
|
||||
if ok and sent then return true end
|
||||
return nil, "log post rejected"
|
||||
end
|
||||
return nil, "no POST transport on this platform"
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user