Commit 940b74b2 authored by johan's avatar johan
Browse files

JNI: asynchronous connection to server supported

parent b7c73451
......@@ -217,39 +217,49 @@ struct jLimeManager {
JavaVM *c_vm;
env.GetJavaVM(&c_vm);
auto callback_lambda = [c_vm, &jstatusObj ](const lime::CallbackReturn status, const std::string message){
// Here we create a global java reference on our object so we won't loose it even if this is called after the current java call
// This global java reference is given in a unique pointer(why??), so just turn it into a shared one so we can copy it into the closure
// We could move it to the closure, but it is itself copied into the create_user argument so it would fail
// Another solution would be to modify the create_user to have it moving the closure and not copying it, it will be used just once anyway
auto jstatusObjRef = std::make_shared<jni::Global<jni::Object<jStatusCallback>, jni::EnvGettingDeleter>>(jni::NewGlobal<jni::EnvGettingDeleter>(env, jstatusObj));
auto callback_lambda = [c_vm, jstatusObjRef](const lime::CallbackReturn status, const std::string message){
// get the env from VM, and retrieve the callback method on StatusCallback class
jni::JNIEnv& g_env { jni::GetEnv(*c_vm)};
auto StatusClass = jni::Class<jStatusCallback>::Find(g_env);
auto StatusMethod = StatusClass.GetMethod<void (jni::jint, jni::String)>(g_env, "callback");
// call the callback on the statusObj we got in param when calling create_user
jstatusObj.Call(g_env, StatusMethod, c2jCallbackReturn(status), jni::Make<jni::String>(g_env, message));
jstatusObjRef->Call(g_env, StatusMethod, c2jCallbackReturn(status), jni::Make<jni::String>(g_env, message));
};
m_manager->create_user( jni::Make<std::string>(env, localDeviceId),
jni::Make<std::string>(env, serverUrl),
j2cCurveId(jcurveId), jOPkInitialBatchSize, std::move(callback_lambda));
j2cCurveId(jcurveId), jOPkInitialBatchSize, callback_lambda);
}
void delete_user(jni::JNIEnv &env, const jni::String &localDeviceId, jni::Object<jStatusCallback> &statusObj ) {
void delete_user(jni::JNIEnv &env, const jni::String &localDeviceId, jni::Object<jStatusCallback> &jstatusObj ) {
LIME_LOGD<<"JNI delete_user user "<<jni::Make<std::string>(env, localDeviceId)<<std::endl;
JavaVM *c_vm;
env.GetJavaVM(&c_vm);
// see create_user for details on this
auto jstatusObjRef = std::make_shared<jni::Global<jni::Object<jStatusCallback>, jni::EnvGettingDeleter>>(jni::NewGlobal<jni::EnvGettingDeleter>(env, jstatusObj));
m_manager->delete_user( jni::Make<std::string>(env, localDeviceId),
[c_vm, &statusObj](const lime::CallbackReturn status, const std::string message){
[c_vm, jstatusObjRef](const lime::CallbackReturn status, const std::string message){
// get the env from VM, and retrieve the callback method on StatusCallback class
jni::JNIEnv& g_env { jni::GetEnv(*c_vm)};
auto StatusClass = jni::Class<jStatusCallback>::Find(g_env);
auto StatusMethod = StatusClass.GetMethod<void (jni::jint, jni::String)>(g_env, "callback");
// call the callback on the statusObj we got in param when calling create_user
statusObj.Call(g_env, StatusMethod, c2jCallbackReturn(status), jni::Make<jni::String>(g_env, message));
jstatusObjRef->Call(g_env, StatusMethod, c2jCallbackReturn(status), jni::Make<jni::String>(g_env, message));
});
}
void encrypt(jni::JNIEnv &env, const jni::String &jlocalDeviceId, const jni::String &jrecipientUserId, jni::Array<jni::Object<jRecipientData>> &jrecipients,
jni::Array<jni::jbyte> &jplainMessage,
jni::Object<jLimeOutputBuffer> &jcipherMessage,
jni::Object<jStatusCallback> &statusObj,
jni::Object<jStatusCallback> &jstatusObj,
jni::jint encryptionPolicy) {
JavaVM *c_vm;
......@@ -279,12 +289,17 @@ struct jLimeManager {
// we must have shared_pointer for recipientUserId
auto recipientUserId = std::make_shared<std::string>(jni::Make<std::string>(env, jrecipientUserId));
// see create_user for details on this
auto jstatusObjRef = std::make_shared<jni::Global<jni::Object<jStatusCallback>, jni::EnvGettingDeleter>>(jni::NewGlobal<jni::EnvGettingDeleter>(env, jstatusObj));
auto jrecipientsRef = std::make_shared<jni::Global<jni::Array<jni::Object<jRecipientData>>, jni::EnvGettingDeleter>>(jni::NewGlobal<jni::EnvGettingDeleter>(env, jrecipients));
auto jcipherMessageRef = std::make_shared<jni::Global<jni::Object<jLimeOutputBuffer>, jni::EnvGettingDeleter>>(jni::NewGlobal<jni::EnvGettingDeleter>(env, jcipherMessage));
m_manager->encrypt(jni::Make<std::string>(env, jlocalDeviceId),
recipientUserId,
recipients,
plainMessage,
cipherMessage,
[c_vm, &statusObj, &jrecipients, &jcipherMessage, recipients, cipherMessage] (const lime::CallbackReturn status, const std::string message) {
[c_vm, jstatusObjRef, jrecipientsRef, jcipherMessageRef, recipients, cipherMessage] (const lime::CallbackReturn status, const std::string message) {
// get the env from VM
jni::JNIEnv& g_env { jni::GetEnv(*c_vm)};
......@@ -295,7 +310,7 @@ struct jLimeManager {
// retrieve the cpp recipients vector and copy back to the jrecipients the peerStatus and DRmessage(if any)
for (size_t i=0; i<recipients->size(); i++) {
auto jrecipient = jrecipients.Get(g_env, i); // recipient is the recipientData javaObject
auto jrecipient = jrecipientsRef->Get(g_env, i); // recipient is the recipientData javaObject
jrecipient.Set(g_env, RecipientDataPeerStatusField, c2jPeerDeviceStatus((*recipients)[i].peerStatus));
auto jDRmessage = jni::Make<jni::Array<jni::jbyte>>(g_env, reinterpret_cast<const std::vector<int8_t>&>((*recipients)[i].DRmessage));
jrecipient.Set(g_env, RecipientDataDRmessageField, jDRmessage);
......@@ -305,17 +320,18 @@ struct jLimeManager {
auto LimeOutputBufferClass = jni::Class<jLimeOutputBuffer>::Find(g_env);
auto LimeOutputBufferField = LimeOutputBufferClass.GetField<jni::Array<jni::jbyte>>(g_env, "buffer");
// get the cipherMessage out
// Can't use directly a byte[] in parameter (as we must create it from c++ code) so use an dedicated class encapsulating a byte[]
auto jcipherMessageArray = jni::Make<jni::Array<jni::jbyte>>(g_env, reinterpret_cast<const std::vector<int8_t>&>(*cipherMessage));
jcipherMessage.Set(g_env, LimeOutputBufferField, jcipherMessageArray);
jcipherMessageRef->Set(g_env, LimeOutputBufferField, jcipherMessageArray);
// retrieve the callback method on StatusCallback class
auto StatusClass = jni::Class<jStatusCallback>::Find(g_env);
auto StatusMethod = StatusClass.GetMethod<void (jni::jint, jni::String)>(g_env, "callback");
// call the callback on the statusObj we got in param when calling create_user
statusObj.Call(g_env, StatusMethod, c2jCallbackReturn(status), jni::Make<jni::String>(g_env, message));
jstatusObjRef->Call(g_env, StatusMethod, c2jCallbackReturn(status), jni::Make<jni::String>(g_env, message));
},
j2cEncryptionPolicy(encryptionPolicy)
);
......@@ -355,13 +371,16 @@ struct jLimeManager {
return c2jPeerDeviceStatus(status);
}
void update(jni::JNIEnv &env, jni::Object<jStatusCallback> &statusObj, jni::jint jOPkServerLowLimit, jni::jint jOPkBatchSize) {
void update(jni::JNIEnv &env, jni::Object<jStatusCallback> &jstatusObj, jni::jint jOPkServerLowLimit, jni::jint jOPkBatchSize) {
JavaVM *c_vm;
env.GetJavaVM(&c_vm);
LIME_LOGD<<"JNI update";
m_manager->update([c_vm, &statusObj] (const lime::CallbackReturn status, const std::string message)
// see create_user for details on this
auto jstatusObjRef = std::make_shared<jni::Global<jni::Object<jStatusCallback>, jni::EnvGettingDeleter>>(jni::NewGlobal<jni::EnvGettingDeleter>(env, jstatusObj));
m_manager->update([c_vm, jstatusObjRef] (const lime::CallbackReturn status, const std::string message)
{
// get the env from VM
jni::JNIEnv& g_env { jni::GetEnv(*c_vm)};
......@@ -371,7 +390,7 @@ struct jLimeManager {
auto StatusMethod = StatusClass.GetMethod<void (jni::jint, jni::String)>(g_env, "callback");
// call the callback on the statusObj we got in param when calling create_user
statusObj.Call(g_env, StatusMethod, c2jCallbackReturn(status), jni::Make<jni::String>(g_env, message));
jstatusObjRef->Call(g_env, StatusMethod, c2jCallbackReturn(status), jni::Make<jni::String>(g_env, message));
},
jOPkServerLowLimit, jOPkBatchSize);
}
......@@ -405,7 +424,7 @@ auto process_X3DHresponse= [](jni::JNIEnv &env, jni::Class<jLimeManager>&, jni::
// turn the response array into a vector of jbytes(signed char)
auto responseVector = std::make_shared<std::vector<uint8_t>>();
jbyteArray2uin8_tVector(env, response, responseVector);
// retrieve the statefull closure pointer to response processing provided by the lime lib
// retrieve the statefull closure pointer to response processing provided by the lime lib
auto responseHolderPtr = reinterpret_cast<responseHolder *>(processPtr);
responseHolderPtr->process(responseCode, *responseVector);
delete(responseHolderPtr);
......
......@@ -23,7 +23,7 @@ find_package(Java REQUIRED)
include(UseJava)
set (LIMETESTER_SOURCE_FILES_JAVA
LimeTester.java
org/linphone/limeTester/LimeTester.java
)
set (LIME_JAR_FILE ${CMAKE_BINARY_DIR}/src/java/Lime.jar)
......@@ -33,4 +33,4 @@ add_jar(LimeTester ${LIMETESTER_SOURCE_FILES_JAVA} INCLUDE_JARS ${LIME_JAR_FILE}
get_target_property(Tester_jarFile LimeTester JAR_FILE)
add_test(NAME JNI COMMAND ${Java_JAVA_EXECUTABLE} -Djava.library.path=${CMAKE_BINARY_DIR}/src -cp ${Tester_jarFile}:${LIME_JAR_FILE} -Xcheck:jni -ea LimeTester)
add_test(NAME JNI COMMAND ${Java_JAVA_EXECUTABLE} -Djava.library.path=${CMAKE_BINARY_DIR}/src -cp ${Tester_jarFile}:${LIME_JAR_FILE} -Xcheck:jni -ea org/linphone/limeTester/LimeTester)
package org.linphone.limeTester;
import org.linphone.lime.*;
import javax.net.ssl.*;
......@@ -11,12 +13,22 @@ import javax.net.ssl.HttpsURLConnection;
import java.io.DataOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.concurrent.*;
/**
* @brief For testing purpose, we just count the success and fail received
* This only mandatory method to implement is callback
* Note: in real situation you may want to implements several variants of
* this interface :
* - dedicated to encrypt call would store reference to in/out buffers
* - used on create_user, delete_user, update would only need to process the
* status.
*/
class LimeStatusCallbackImpl implements LimeStatusCallback {
public int success;
public int fail;
public int timeout;
/**
* @brief Function called by native code when asynchronous processing is completed
......@@ -36,17 +48,38 @@ class LimeStatusCallbackImpl implements LimeStatusCallback {
public void resetStatus() {
success = 0;
fail = 0;
timeout = 4000;
}
public LimeStatusCallbackImpl() {
resetStatus();
}
public boolean wait_for_success(int expected_success) {
try {
int time = 0;
while (time<timeout && success<expected_success) {
time += 25;
Thread.sleep(25);
}
}
catch (InterruptedException e) {
System.out.println("Interrupt exception in wait for success");
return false;
}
if (expected_success == success) {
return true;
} else {
return false;
}
}
}
class LimePostToX3DH_Sync implements LimePostToX3DH {
/** @brief Function call by native side to post a message to an X3DH server.
* The connection is synchronous so this function also collect the answer
* and send it back to the native library using the process_response native method
* and send it back to the native library using the LimeManager.process_response native method
*/
public void postToX3DHServer(long ptr, String url, String from, byte[] message) {
try {
......@@ -61,6 +94,7 @@ class LimePostToX3DH_Sync implements LimePostToX3DH {
con.setRequestProperty("Content-type", "x3dh/octet-stream");
con.setRequestProperty("From", from);
// Send post request
con.setDoOutput(true);
DataOutputStream wr = new DataOutputStream(con.getOutputStream());
......@@ -91,75 +125,143 @@ class LimePostToX3DH_Sync implements LimePostToX3DH {
}
}
public class LimeTester {
public static void main(String[] args) {
// Load lime library
class RunnablePostToHttp implements Runnable {
private Thread t;
private long m_ptr;
private String m_url;
private String m_from;
private byte[] m_message;
RunnablePostToHttp( long ptr, String url, String from, byte[] message) {
m_ptr = ptr;
m_url = url;
m_from = from;
m_message = message;
}
public void run() {
try {
System.loadLibrary("lime");
} catch (Exception e) {
System.out.println("Exception while loading peer library" + e.getMessage());
System.exit(-1);
}
// connect to the given URL
String local_url = new String(m_url.toCharArray());
URL obj = new URL(local_url);
HttpsURLConnection con = (HttpsURLConnection) obj.openConnection();
// delete testing db files
File file = new File("testdb_Alice.sqlite3");
file.delete();
file = new File("testdb_Bob.sqlite3");
file.delete();
// set request header
con.setRequestMethod("POST");
con.setRequestProperty("User-Agent", "lime");
con.setRequestProperty("Content-type", "x3dh/octet-stream");
con.setRequestProperty("From", m_from);
// INSECURE CODE - USED FOR TESTING ONLY - DO NOT USE THIS IN REAL SITUATION
// Disable SSL certificate verification
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { }
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { }
// Send post request
con.setDoOutput(true);
DataOutputStream wr = new DataOutputStream(con.getOutputStream());
wr.write(m_message, 0, m_message.length);
wr.flush();
wr.close();
// wait for a response
int responseCode = con.getResponseCode();
InputStream in = con.getInputStream();
ByteArrayOutputStream response = new ByteArrayOutputStream( );
byte[] buffer = new byte[256];
int bufferLength;
while ((bufferLength = in.read(buffer)) != -1){
response.write(buffer, 0, bufferLength);
}
};
in.close();
try {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (Exception e) {
System.out.println("Exception while initializing the trustAll certificates" + e.getMessage());
System.exit(-1);
// call response process native function
LimeManager.process_X3DHresponse(m_ptr, responseCode, response.toByteArray());
response.close();
}
// End of INSECURE CODE - USED FOR TESTING ONLY - DO NOT USE THIS IN REAL SITUATION
catch (Exception e) {
System.out.println("Thread interrupted : "+ e.getMessage());
}
}
}
class LimePostToX3DH_Async implements LimePostToX3DH {
/** @brief Function call by native side to post a message to an X3DH server.
* The connection is asynchronous : when called the post function starts a new thread
* and makes it request in it, then collect the answer
* and send it back to the native library using the LimeManager.process_response native method
*/
public void postToX3DHServer(long ptr, String url, String from, byte[] message) {
RunnablePostToHttp poster = new RunnablePostToHttp(ptr, url, from, message);
new Thread(poster).start();
}
}
public class LimeTester {
/**
* Test Scenario:
* - Create Alice and Bob users
* - Alice encrypts to Bob who decrypts and check it matches the original
* - Bob encryps to Alice who decrypts and check it matches the original
* - Alice and Bob call the update function
*/
public static void hello_world(LimeCurveId curveId, String dbBasename, String x3dhServerUrl, LimePostToX3DH postObj) {
int expected_success = 0;
int expected_fail = 0;
// Create a callback
LimeStatusCallbackImpl statusCallback = new LimeStatusCallbackImpl();
LimePostToX3DH_Sync postObj = new LimePostToX3DH_Sync();
// Create random device id for alice and bob
String AliceDeviceId = "alice"+UUID.randomUUID().toString();
String BobDeviceId = "bob"+UUID.randomUUID().toString();
LimeManager aliceManager = new LimeManager("testdb_Alice.sqlite3", postObj);
LimeManager bobManager = new LimeManager("testdb_Bob.sqlite3", postObj);
// Create db filenames and delete potential existing ones
String curveIdString;
if (curveId == LimeCurveId.C25519) {
curveIdString = ".C25519.";
} else {
curveIdString = ".C448.";
}
String aliceDbFilename = "alice."+dbBasename+curveIdString+".sqlite3";
String bobDbFilename = "bob."+dbBasename+curveIdString+".sqlite3";
File file = new File(aliceDbFilename);
file.delete();
file = new File(bobDbFilename);
file.delete();
// Create bob and alice lime managers
LimeManager aliceManager = new LimeManager(aliceDbFilename, postObj);
LimeManager bobManager = new LimeManager(bobDbFilename, postObj);
// Use this managers to create users
expected_success+= 2;
aliceManager.create_user(AliceDeviceId, "https://localhost:25519", LimeCurveId.C25519, statusCallback);
bobManager.create_user(BobDeviceId, "https://localhost:25519", LimeCurveId.C25519, 10, statusCallback);
assert (statusCallback.success == expected_success);
aliceManager.create_user(AliceDeviceId, x3dhServerUrl, curveId, statusCallback);
bobManager.create_user(BobDeviceId, x3dhServerUrl, curveId, 10, statusCallback);
assert (statusCallback.wait_for_success(expected_success));
assert (statusCallback.fail == expected_fail);
// prepare for encrypt
// prepare for encrypt: An array of RecipientData, one element for each recipient device
// encrypted output will be stored in it
RecipientData[] recipients = new RecipientData[1]; // 1 target for the encrypt
String plainMessage = "Moi je connais un ami il s'appelle Alceste";
recipients[0] = new RecipientData(BobDeviceId);
LimeOutputBuffer cipherMessage = new LimeOutputBuffer();
// encrypts, the plain message is a byte array so we can encrypt anything
// this call is asynchronous (unless you use an synchronous connection to the X3DH server)
// and the statusCallback.callback function will be called when the encryption is done
expected_success+= 1;
aliceManager.encrypt(AliceDeviceId, "bob", recipients, plainMessage.getBytes(), cipherMessage, statusCallback);
// here we wait for the status callback to be called but a full asynchronous process would
// store references to recipients and cipherMessage in the statusCallback object and process
// them from the callback method.
assert (statusCallback.wait_for_success(expected_success));
assert (recipients[0].getPeerStatus() == LimePeerDeviceStatus.UNKNOWN):"Encryption status for Bob is expected to be UNKNOWN"; // Bob is unknown in Alice's base
assert (cipherMessage.buffer.length==0):"Default encryption policy implies no cipherMessage for one recipient only";
assert (statusCallback.success == expected_success);
assert (statusCallback.fail == expected_fail);
LimeOutputBuffer decodedMessage = new LimeOutputBuffer();
// decrypt and check we get back to the original
// decryption is synchronous, no callback on this one
LimePeerDeviceStatus status = bobManager.decrypt(BobDeviceId, "bob", AliceDeviceId, recipients[0].DRmessage, decodedMessage);
String s = new String(decodedMessage.buffer);
assert (s.equals(plainMessage)):"Decoded message is not the encoded one";
......@@ -173,9 +275,9 @@ public class LimeTester {
expected_success+= 1;
bobManager.encrypt(BobDeviceId, "alice", recipients, plainMessage.getBytes(), cipherMessage, statusCallback, LimeEncryptionPolicy.CIPHERMESSAGE);
assert (recipients[0].getPeerStatus() == LimePeerDeviceStatus.UNTRUSTED):"Encryption status for Bob is expected to be UNTRUSTED"; // Alice status is untrusted in Bob's base
assert (statusCallback.wait_for_success(expected_success));
assert (recipients[0].getPeerStatus() == LimePeerDeviceStatus.UNTRUSTED):"Encryption status for Alice is expected to be UNTRUSTED"; // Alice status is untrusted in Bob's base
assert (cipherMessage.buffer.length>0):"Forced cipherMessage encryption policy must produce a cipherMessage even for one recipient only";
assert (statusCallback.success == expected_success);
assert (statusCallback.fail == expected_fail);
......@@ -189,20 +291,65 @@ public class LimeTester {
expected_success+= 2;
aliceManager.update(statusCallback);
bobManager.update(statusCallback);
assert (statusCallback.success == expected_success);
assert (statusCallback.wait_for_success(expected_success));
assert (statusCallback.fail == expected_fail);
// clean db
expected_success+= 2;
aliceManager.delete_user(AliceDeviceId, statusCallback);
bobManager.delete_user(BobDeviceId, statusCallback);
assert (statusCallback.success == expected_success);
assert (statusCallback.wait_for_success(expected_success));
assert (statusCallback.fail == expected_fail);
// Force garbage collection to be sure the native object destructor is called
aliceManager = null;
bobManager = null;
System.gc();
}
public static void main(String[] args) {
// Load lime library
try {
System.loadLibrary("lime");
} catch (Exception e) {
System.out.println("Exception while loading peer library" + e.getMessage());
System.exit(-1);
}
// INSECURE CODE - USED FOR TESTING ONLY - DO NOT USE THIS IN REAL SITUATION
// Disable SSL certificate verification
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { }
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { }
}
};
try {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (Exception e) {
System.out.println("Exception while initializing the trustAll certificates" + e.getMessage());
System.exit(-1);
}
// End of INSECURE CODE - USED FOR TESTING ONLY - DO NOT USE THIS IN REAL SITUATION
/**
* Hello World test:
* - mostly a code demo. More details in the c++ hello world test, but it is a good start
* Run it synchronous and asynchronous on both curves.
*/
LimePostToX3DH_Sync sync_postObj = new LimePostToX3DH_Sync();
hello_world(LimeCurveId.C25519, "hello_world", "https://localhost:25519", sync_postObj);
hello_world(LimeCurveId.C448, "hello_world", "https://localhost:25520", sync_postObj);
LimePostToX3DH_Async async_postObj = new LimePostToX3DH_Async();
hello_world(LimeCurveId.C25519, "hello_world", "https://localhost:25519", async_postObj);
hello_world(LimeCurveId.C448, "hello_world", "https://localhost:25520", async_postObj);
System.exit(0);
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment