Browse Source

Merge branch 'android'

Miku AuahDark 3 years ago
parent
commit
6b3da2d73c

+ 26 - 0
Android.mk

@@ -0,0 +1,26 @@
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE    := https
+LOCAL_MODULE_FILENAME := https
+
+LOCAL_CFLAGS    := -DNOMINMAX
+LOCAL_CPPFLAGS  := -std=c++11
+
+LOCAL_ARM_NEON := true
+
+LOCAL_C_INCLUDES := \
+	${LOCAL_PATH}/src \
+	${LOCAL_PATH}/src/android \
+	${LOCAL_PATH}/src/android/ndk-build
+
+LOCAL_SRC_FILES := \
+	src/lua/main.cpp \
+	src/common/HTTPRequest.cpp \
+	src/common/HTTPSClient.cpp \
+	src/common/PlaintextConnection.cpp \
+	src/android/AndroidClient.cpp
+
+LOCAL_SHARED_LIBRARIES := liblove
+
+include $(BUILD_SHARED_LIBRARY)

+ 1 - 0
java.txt

@@ -0,0 +1 @@
+src/android/java

+ 21 - 1
src/CMakeLists.txt

@@ -45,19 +45,25 @@ add_library (https-nsurl STATIC EXCLUDE_FROM_ALL
 	macos/NSURLClient.mm
 )
 
+add_library (https-android STATIC EXCLUDE_FROM_ALL
+	android/AndroidClient.cpp
+)
+
 ### Flags
 if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
 	option (USE_CURL_BACKEND "Use the libcurl backend" ON)
 	option (USE_OPENSSL_BACKEND "Use the openssl backend" ON)
 	option (USE_SCHANNEL_BACKEND "Use the schannel backend (windows-only)" OFF)
 	option (USE_NSURL_BACKEND "Use the NSUrl backend (macos-only)" OFF)
+	option (USE_ANDROID_BACKEND "Use the Android Java backend (Android-only)" OFF)
 
 	option (USE_WINSOCK "Use winsock instead of BSD sockets (windows-only)" OFF)
-elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+elseif (WIN32)
 	option (USE_CURL_BACKEND "Use the libcurl backend" OFF)
 	option (USE_OPENSSL_BACKEND "Use the openssl backend" OFF)
 	option (USE_SCHANNEL_BACKEND "Use the schannel backend (windows-only)" ON)
 	option (USE_NSURL_BACKEND "Use the NSUrl backend (macos-only)" OFF)
+	option (USE_ANDROID_BACKEND "Use the Android Java backend (Android-only)" OFF)
 
 	option (USE_WINSOCK "Use winsock instead of BSD sockets (windows-only)" ON)
 elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
@@ -65,6 +71,15 @@ elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
 	option (USE_OPENSSL_BACKEND "Use the openssl backend" OFF)
 	option (USE_SCHANNEL_BACKEND "Use the schannel backend (windows-only)" OFF)
 	option (USE_NSURL_BACKEND "Use the NSUrl backend (macos-only)" ON)
+	option (USE_ANDROID_BACKEND "Use the Android Java backend (Android-only)" OFF)
+
+	option (USE_WINSOCK "Use winsock instead of BSD sockets (windows-only)" OFF)
+elseif (ANDROID)
+	option (USE_CURL_BACKEND "Use the libcurl backend" OFF)
+	option (USE_OPENSSL_BACKEND "Use the openssl backend" OFF)
+	option (USE_SCHANNEL_BACKEND "Use the schannel backend (windows-only)" OFF)
+	option (USE_NSURL_BACKEND "Use the NSUrl backend (macos-only)" OFF)
+	option (USE_ANDROID_BACKEND "Use the Android Java backend (Android-only)" ON)
 
 	option (USE_WINSOCK "Use winsock instead of BSD sockets (windows-only)" OFF)
 endif ()
@@ -106,6 +121,11 @@ if (USE_NSURL_BACKEND)
 	target_link_libraries (https https-nsurl)
 endif ()
 
+if (USE_ANDROID_BACKEND)
+	target_link_libraries (https https-android)
+	message(STATUS "Ensure to add the Java files to your project too!")
+endif ()
+
 ### Generate config.h
 configure_file (
 	common/config.h.in

+ 212 - 0
src/android/AndroidClient.cpp

@@ -0,0 +1,212 @@
+#include "AndroidClient.h"
+
+#ifdef USE_ANDROID_BACKEND
+
+#include <sstream>
+#include <type_traits>
+
+#include <dlfcn.h>
+
+static std::string replace(const std::string &str, const std::string &from, const std::string &to)
+{
+	std::stringstream ss;
+	size_t oldpos = 0;
+
+	while (true)
+	{
+		size_t pos = str.find(from, oldpos);
+
+		if (pos == std::string::npos)
+		{
+			ss << str.substr(oldpos);
+			break;
+		}
+
+		ss << str.substr(oldpos, pos - oldpos) << to;
+		oldpos = pos + from.length();
+	}
+
+	return ss.str();
+}
+
+static jstring newStringUTF(JNIEnv *env, const std::string &str)
+{
+	// We want std::string that contains null byte, hence length of 1.
+	static std::string null("", 1);
+
+	std::string newStr = replace(str, null, "\xC0\x80");
+	jstring jstr = env->NewStringUTF(newStr.c_str());
+	return jstr;
+}
+
+static std::string getStringUTF(JNIEnv *env, jstring str)
+{
+	// We want std::string that contains null byte, hence length of 1.
+	static std::string null("", 1);
+
+	const char *c = env->GetStringUTFChars(str, nullptr);
+	std::string result = replace(c, "\xC0\x80", null);
+
+	env->ReleaseStringUTFChars(str, c);
+	return result;
+}
+
+AndroidClient::AndroidClient()
+: HTTPSClient()
+, SDL_AndroidGetJNIEnv(nullptr)
+{
+	// Look for SDL_AndroidGetJNIEnv
+	SDL_AndroidGetJNIEnv = (decltype(SDL_AndroidGetJNIEnv)) dlsym(RTLD_DEFAULT, "SDL_AndroidGetJNIEnv");
+	// Look for SDL_AndroidGetActivity
+	SDL_AndroidGetActivity = (decltype(SDL_AndroidGetActivity)) dlsym(RTLD_DEFAULT, "SDL_AndroidGetActivity");
+}
+
+bool AndroidClient::valid() const
+{
+	if (SDL_AndroidGetJNIEnv && SDL_AndroidGetActivity)
+	{
+		JNIEnv *env = SDL_AndroidGetJNIEnv();
+
+		if (env)
+		{
+			jclass httpsClass = getHTTPSClass();
+			if (env->ExceptionCheck())
+			{
+				env->ExceptionClear();
+				return false;
+			}
+
+			env->DeleteLocalRef(httpsClass);
+			return true;
+		}
+	}
+
+	return false;
+}
+
+HTTPSClient::Reply AndroidClient::request(const HTTPSClient::Request &req)
+{
+	JNIEnv *env = SDL_AndroidGetJNIEnv();
+	jclass httpsClass = getHTTPSClass();
+
+	if (httpsClass == nullptr)
+	{
+		env->ExceptionClear();
+		throw std::runtime_error("Could not find class 'org.love2d.luahttps.LuaHTTPS'");
+	}
+
+	jmethodID constructor = env->GetMethodID(httpsClass, "<init>", "()V");
+	jmethodID setURL = env->GetMethodID(httpsClass, "setUrl", "(Ljava/lang/String;)V");
+	jmethodID request = env->GetMethodID(httpsClass, "request", "()Z");
+	jmethodID getInterleavedHeaders = env->GetMethodID(httpsClass, "getInterleavedHeaders", "()[Ljava/lang/String;");
+	jmethodID getResponse = env->GetMethodID(httpsClass, "getResponse", "()[B");
+	jmethodID getResponseCode = env->GetMethodID(httpsClass, "getResponseCode", "()I");
+
+	jobject httpsObject = env->NewObject(httpsClass, constructor);
+
+	// Set URL
+	jstring url = env->NewStringUTF(req.url.c_str());
+	env->CallVoidMethod(httpsObject, setURL, url);
+	env->DeleteLocalRef(url);
+
+	// Set post data
+	if (req.method == Request::POST)
+	{
+		jmethodID setPostData = env->GetMethodID(httpsClass, "setPostData", "([B)V");
+		jbyteArray byteArray = env->NewByteArray((jsize) req.postdata.length());
+		jbyte *byteArrayData = env->GetByteArrayElements(byteArray, nullptr);
+
+		memcpy(byteArrayData, req.postdata.data(), req.postdata.length());
+		env->ReleaseByteArrayElements(byteArray, byteArrayData, 0);
+
+		env->CallVoidMethod(httpsObject, setPostData, byteArray);
+		env->DeleteLocalRef(byteArray);
+	}
+
+	// Set headers
+	if (!req.headers.empty())
+	{
+		jmethodID addHeader = env->GetMethodID(httpsClass, "addHeader", "(Ljava/lang/String;Ljava/lang/String;)V");
+
+		for (auto &header : req.headers)
+		{
+			jstring headerKey = newStringUTF(env, header.first);
+			jstring headerValue = newStringUTF(env, header.second);
+
+			env->CallVoidMethod(httpsObject, addHeader, headerKey, headerValue);
+			env->DeleteLocalRef(headerKey);
+			env->DeleteLocalRef(headerValue);
+		}
+	}
+
+	// Do request
+	HTTPSClient::Reply response;
+	jboolean status = env->CallBooleanMethod(httpsObject, request);
+
+	// Get response
+	response.responseCode = env->CallIntMethod(httpsObject, getResponseCode);
+
+	if (status)
+	{
+		// Get headers
+		jobjectArray interleavedHeaders = (jobjectArray) env->CallObjectMethod(httpsObject, getInterleavedHeaders);
+		int len = env->GetArrayLength(interleavedHeaders);
+
+		for (int i = 0; i < len; i += 2)
+		{
+			jstring key = (jstring) env->GetObjectArrayElement(interleavedHeaders, i);
+			jstring value = (jstring) env->GetObjectArrayElement(interleavedHeaders, i + 1);
+
+			response.headers[getStringUTF(env, key)] = getStringUTF(env, value);
+
+			env->DeleteLocalRef(key);
+			env->DeleteLocalRef(value);
+		}
+
+		env->DeleteLocalRef(interleavedHeaders);
+
+		// Get response data
+		jbyteArray responseData = (jbyteArray) env->CallObjectMethod(httpsObject, getResponse);
+
+		if (responseData)
+		{
+			int len = env->GetArrayLength(responseData);
+			jbyte *responseByte = env->GetByteArrayElements(responseData, nullptr);
+
+			response.body = std::string((char *) responseByte, len);
+
+			env->DeleteLocalRef(responseData);
+		}
+	}
+
+	return response;
+}
+
+jclass AndroidClient::getHTTPSClass() const
+{
+	JNIEnv *env = SDL_AndroidGetJNIEnv();
+
+	jclass classLoaderClass = env->FindClass("java/lang/ClassLoader");
+	jmethodID loadClass = env->GetMethodID(classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
+
+	jobject activity = SDL_AndroidGetActivity();
+
+	if (activity == nullptr)
+		return nullptr;
+
+	jclass gameActivity = env->GetObjectClass(activity);
+	jmethodID getLoader = env->GetMethodID(gameActivity, "getClassLoader", "()Ljava/lang/ClassLoader;");
+	jobject classLoader = env->CallObjectMethod(activity, getLoader);
+
+	jstring httpsClassName = env->NewStringUTF("org.love2d.luahttps.LuaHTTPS");
+	jclass httpsClass = (jclass) env->CallObjectMethod(classLoader, loadClass, httpsClassName);
+
+	env->DeleteLocalRef(gameActivity);
+	env->DeleteLocalRef(httpsClassName);
+	env->DeleteLocalRef(activity);
+	env->DeleteLocalRef(classLoaderClass);
+
+	return httpsClass;
+}
+
+#endif

+ 26 - 0
src/android/AndroidClient.h

@@ -0,0 +1,26 @@
+#pragma once
+
+#include "common/config.h"
+
+#ifdef USE_ANDROID_BACKEND
+
+#include <jni.h>
+
+#include "common/HTTPSClient.h"
+
+class AndroidClient: public HTTPSClient
+{
+public:
+	AndroidClient();
+
+	bool valid() const override;
+	HTTPSClient::Reply request(const HTTPSClient::Request &req) override;
+
+private:
+	JNIEnv *(*SDL_AndroidGetJNIEnv)();
+	jobject (*SDL_AndroidGetActivity)();
+
+	jclass getHTTPSClass() const;
+};
+
+#endif

+ 171 - 0
src/android/java/org/love2d/luahttps/LuaHTTPS.java

@@ -0,0 +1,171 @@
+package org.love2d.luahttps;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.Keep;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Keep
+class LuaHTTPS {
+    static private String TAG = "LuaHTTPS";
+
+    private String urlString;
+    private byte[] postData;
+    private byte[] response;
+    private int responseCode;
+    private HashMap<String, String> headers;
+
+    public LuaHTTPS() {
+        headers = new HashMap<String, String>();
+        reset();
+    }
+
+    public void reset() {
+        urlString = null;
+        postData = null;
+        response = null;
+        responseCode = 0;
+        headers.clear();
+    }
+
+    @Keep
+    public void setUrl(String url) {
+        urlString = url;
+    }
+
+    @Keep
+    public void setPostData(byte[] postData) {
+        this.postData = postData;
+    }
+
+    @Keep
+    public void addHeader(String key, String value) {
+        headers.put(key, value);
+    }
+
+    @Keep
+    public String[] getInterleavedHeaders() {
+        ArrayList<String> resultInterleaved = new ArrayList<String>();
+
+        for (Map.Entry<String, String> header: headers.entrySet()) {
+            String key = header.getKey();
+            String value = header.getValue();
+
+            if (key != null && value != null) {
+                resultInterleaved.add(key);
+                resultInterleaved.add(value);
+            }
+        }
+
+        String[] result = new String[resultInterleaved.size()];
+        resultInterleaved.toArray(result);
+        return result;
+    }
+
+    @Keep
+    public int getResponseCode() {
+        return responseCode;
+    }
+
+    @Keep
+    public byte[] getResponse() {
+        return response;
+    }
+
+    @Keep
+    public boolean request() {
+        if (urlString == null) {
+            return false;
+        }
+
+        URL url;
+        try {
+            url = new URL(urlString);
+
+            if (!url.getProtocol().equals("http") && !url.getProtocol().equals("https")) {
+                return false;
+            }
+        } catch (MalformedURLException e) {
+            Log.e(TAG, "Error", e);
+            return false;
+        }
+
+        HttpURLConnection connection;
+        try {
+            connection = (HttpURLConnection) url.openConnection();
+        } catch (IOException e) {
+            Log.e(TAG, "Error", e);
+            return false;
+        }
+
+        // Set header
+        for (Map.Entry<String, String> headerData: headers.entrySet()) {
+            connection.setRequestProperty(headerData.getKey(), headerData.getValue());
+        }
+
+        // Set post data
+        if (postData != null) {
+            connection.setDoOutput(true);
+            connection.setChunkedStreamingMode(0);
+
+            try {
+                OutputStream out = connection.getOutputStream();
+                out.write(postData);
+            } catch (Exception e) {
+                Log.e(TAG, "Error", e);
+                connection.disconnect();
+                return false;
+            }
+        }
+
+        // Request
+        try {
+            InputStream in;
+
+            // Set response code
+            responseCode = connection.getResponseCode();
+            if (responseCode >= 400) {
+                in = connection.getErrorStream();
+            } else {
+                in = connection.getInputStream();
+            }
+
+            // Read response
+            int readed;
+            byte[] temp = new byte[4096];
+            ByteArrayOutputStream response = new ByteArrayOutputStream();
+
+            while ((readed = in.read(temp)) != -1) {
+                response.write(temp, 0, readed);
+            }
+
+            this.response = response.toByteArray();
+            response.close();
+
+            // Read headers
+            headers.clear();
+            for (Map.Entry<String, List<String>> header: connection.getHeaderFields().entrySet()) {
+                headers.put(header.getKey(), TextUtils.join(", ", header.getValue()));
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Error", e);
+            connection.disconnect();
+            return false;
+        }
+
+        connection.disconnect();
+        return true;
+    }
+}

+ 2 - 0
src/android/ndk-build/common/config.h

@@ -0,0 +1,2 @@
+#define USE_ANDROID_BACKEND
+#define DLLEXPORT __attribute__((visibility ("default")))

+ 1 - 0
src/common/config.h.in

@@ -2,6 +2,7 @@
 #cmakedefine USE_OPENSSL_BACKEND
 #cmakedefine USE_SCHANNEL_BACKEND
 #cmakedefine USE_NSURL_BACKEND
+#cmakedefine USE_ANDROID_BACKEND
 #cmakedefine USE_WINSOCK
 #define DLLEXPORT @DLLEXPORT@
 #cmakedefine DEBUG_SCHANNEL

+ 9 - 0
src/lua/main.cpp

@@ -16,6 +16,9 @@
 #ifdef USE_NSURL_BACKEND
 #	include "macos/NSURLClient.h"
 #endif
+#ifdef USE_ANDROID_BACKEND
+#	include "android/AndroidClient.h"
+#endif
 
 #ifdef USE_CURL_BACKEND
 	static CurlClient curlclient;
@@ -29,6 +32,9 @@
 #ifdef USE_NSURL_BACKEND
 	static NSURLClient nsurlclient;
 #endif
+#ifdef USE_ANDROID_BACKEND
+	static AndroidClient androidclient;
+#endif
 
 static HTTPSClient *clients[] = {
 #ifdef USE_CURL_BACKEND
@@ -42,6 +48,9 @@ static HTTPSClient *clients[] = {
 #endif
 #ifdef USE_NSURL_BACKEND
 	&nsurlclient,
+#endif
+#ifdef USE_ANDROID_BACKEND
+	&androidclient,
 #endif
 	nullptr,
 };