mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 20:50:21 +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;
|
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
|
* TLS sockets. Same resolution rule as httpDownload above -- the activity's
|
||||||
* own class, never FindClass -- and the same tolerance for an old APK: a
|
* 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);
|
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).
|
* TLS client sockets (GameActivity.tls*, implemented by TlsSocket.java).
|
||||||
* LuaSocket, which is what LOVE ships, does TCP only, so wss:// is otherwise
|
* 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
|
#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
|
int System::tlsOpen(const char *host, int port) const
|
||||||
{
|
{
|
||||||
#ifdef LOVE_ANDROID
|
#ifdef LOVE_ANDROID
|
||||||
|
|||||||
@@ -151,6 +151,14 @@ 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* TLS client sockets (Android only; every call fails elsewhere, where
|
||||||
* LuaSec or another provider is the answer). Non-blocking by contract:
|
* LuaSec or another provider is the answer). Non-blocking by contract:
|
||||||
|
|||||||
@@ -139,6 +139,17 @@ int w_httpDownload(lua_State *L)
|
|||||||
return 1;
|
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)
|
int w_hasBackgroundMusic(lua_State *L)
|
||||||
{
|
{
|
||||||
lua_pushboolean(L, instance()->hasBackgroundMusic());
|
lua_pushboolean(L, instance()->hasBackgroundMusic());
|
||||||
@@ -233,6 +244,7 @@ static const luaL_Reg functions[] =
|
|||||||
{ "syncHealthSteps", w_syncHealthSteps },
|
{ "syncHealthSteps", w_syncHealthSteps },
|
||||||
{ "restartApp", w_restartApp },
|
{ "restartApp", w_restartApp },
|
||||||
{ "httpDownload", w_httpDownload },
|
{ "httpDownload", w_httpDownload },
|
||||||
|
{ "httpPost", w_httpPost },
|
||||||
{ "tlsOpen", w_tlsOpen },
|
{ "tlsOpen", w_tlsOpen },
|
||||||
{ "tlsStatus", w_tlsStatus },
|
{ "tlsStatus", w_tlsStatus },
|
||||||
{ "tlsSend", w_tlsSend },
|
{ "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
|
* Shows ACTION_CREATE_DOCUMENT so the player can save a staged export
|
||||||
* (pending_export.sav in the app save identity) to Downloads / Drive /
|
* (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
|
-- POST returning success/failure. Strictly one-way: the response body is
|
||||||
-- discarded, only the HTTP status class is surfaced (postLog callers never
|
-- 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
|
-- 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
|
-- large body never lands in the command line; where curl is absent (Android
|
||||||
-- transport, and httpPost reports that instead of half-working through
|
-- and the other bridge-only platforms) the POST rides the JNI bridge --
|
||||||
-- httpDownload (a GET round-trip to a POST endpoint would be a lie).
|
-- 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)
|
function HostShell.httpPost(url, body, contentType, userAgent, maxTime)
|
||||||
if type(url) ~= "string" or url == "" then return nil, "missing url" end
|
if type(url) ~= "string" or url == "" then return nil, "missing url" end
|
||||||
if type(body) ~= "string" then return nil, "missing body" 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
|
if not haveBridge() then
|
||||||
return nil, "no network transport on this platform"
|
return nil, "no network transport on this platform"
|
||||||
end
|
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"
|
return nil, "no POST transport on this platform"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user