0
0
mirror of https://github.com/signalapp/libsignal.git synced 2024-09-19 19:42:19 +02:00

libsignal-net: ChatService jni bridge

This commit is contained in:
Sergey Skrobotov 2024-03-21 12:06:19 -07:00
parent 23764a50e8
commit d7a4b8c817
18 changed files with 583 additions and 57 deletions

7
java/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
# IntelliJ specific files/directories
out
.shelf
.idea
*.ipr
*.iws
*.iml

View File

@ -0,0 +1,154 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net;
import java.net.MalformedURLException;
import java.util.Map;
import org.signal.libsignal.internal.CalledFromNative;
import org.signal.libsignal.internal.CompletableFuture;
import org.signal.libsignal.internal.FilterExceptions;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
/**
* Represents an API of communication with the Chat Service.
*
* <p>An instance of this object is obtained via call to {@link Network#createChatService(String,
* String)} method.
*/
public class ChatService extends NativeHandleGuard.SimpleOwner {
private final TokioAsyncContext tokioAsyncContext;
ChatService(
final TokioAsyncContext tokioAsyncContext,
final Network.ConnectionManager connectionManager,
final String username,
final String password) {
super(
connectionManager.guardedMap(
connectionManagerHandle ->
Native.ChatService_new(connectionManagerHandle, username, password)));
this.tokioAsyncContext = tokioAsyncContext;
}
/**
* Initiates termination of the underlying connection to the Chat Service. After the service is
* disconnected, it will not attempt to automatically reconnect until one of the request methods
* is used (e.g. {@link #unauthenticatedSend(Request)}).
*
* <p>Note: the same instance of {@code ChatService} can be reused after {@code disconnect()} was
* called.
*
* @return a future that completes when the underlying connection is terminated.
*/
@SuppressWarnings("unchecked")
public CompletableFuture<Void> disconnect() {
return tokioAsyncContext.guardedMap(
asyncContextHandle ->
guardedMap(
chatServiceHandle ->
Native.ChatService_disconnect(asyncContextHandle, chatServiceHandle)));
}
/**
* Sends request to the Chat Service over an unauthenticated channel.
*
* @param req request object
* @return a {@code CompletableFuture} of a {@link Response}
* @throws MalformedURLException is thrown if {@code pathAndQuery} component of the request has an
* invalid structure.
*/
public CompletableFuture<Response> unauthenticatedSend(final Request req)
throws MalformedURLException {
final InternalRequest internalRequest = buildInternalRequest(req);
try (final NativeHandleGuard asyncContextHandle = new NativeHandleGuard(tokioAsyncContext);
final NativeHandleGuard chatServiceHandle = new NativeHandleGuard(this);
final NativeHandleGuard requestHandle = new NativeHandleGuard(internalRequest)) {
return Native.ChatService_unauth_send(
asyncContextHandle.nativeHandle(),
chatServiceHandle.nativeHandle(),
requestHandle.nativeHandle(),
req.timeoutMillis)
.thenApply(o -> (Response) o);
}
}
/**
* Sends request to the Chat Service over an unauthenticated channel.
*
* <p>In addition to the response, an object containing debug information about the request flow
* is returned.
*
* @param req request object
* @return a {@code CompletableFuture} of a {@link ResponseAndDebugInfo}
* @throws MalformedURLException is thrown if {@code pathAndQuery} component of the request has an
* invalid structure.
*/
public CompletableFuture<ResponseAndDebugInfo> unauthenticatedSendAndDebug(final Request req)
throws MalformedURLException {
final InternalRequest internalRequest = buildInternalRequest(req);
try (final NativeHandleGuard asyncContextHandle = new NativeHandleGuard(tokioAsyncContext);
final NativeHandleGuard chatServiceHandle = new NativeHandleGuard(this);
final NativeHandleGuard requestHandle = new NativeHandleGuard(internalRequest)) {
return Native.ChatService_unauth_send_and_debug(
asyncContextHandle.nativeHandle(),
chatServiceHandle.nativeHandle(),
requestHandle.nativeHandle(),
req.timeoutMillis)
.thenApply(o -> (ResponseAndDebugInfo) o);
}
}
static InternalRequest buildInternalRequest(final Request req) throws MalformedURLException {
final InternalRequest result =
new InternalRequest(req.method(), req.pathAndQuery(), req.body());
req.headers().forEach(result::addHeader);
return result;
}
@Override
protected void release(final long nativeHandle) {
Native.Chat_Destroy(nativeHandle);
}
static class InternalRequest extends NativeHandleGuard.SimpleOwner {
InternalRequest(final String method, final String pathAndQuery, final byte[] body)
throws MalformedURLException {
super(
FilterExceptions.filterExceptions(
MalformedURLException.class,
() -> Native.HttpRequest_new(method, pathAndQuery, body)));
}
@Override
protected void release(final long nativeHandle) {
Native.HttpRequest_Destroy(nativeHandle);
}
public void addHeader(final String name, final String value) {
guardedRun(h -> Native.HttpRequest_add_header(h, name, value));
}
}
public record Request(
String method,
String pathAndQuery,
Map<String, String> headers,
byte[] body,
int timeoutMillis) {}
public record Response(int status, String message, Map<String, String> headers, byte[] body) {}
public record DebugInfo(boolean connectionReused, int reconnectCount, IpType ipType) {
@CalledFromNative
DebugInfo(boolean connectionReused, int reconnectCount, byte ipTypeCode) {
this(connectionReused, reconnectCount, IpType.values()[ipTypeCode]);
}
}
public record ResponseAndDebugInfo(Response response, DebugInfo debugInfo) {}
}

View File

@ -0,0 +1,13 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net;
/** Error thrown by Chat Service API. */
public class ChatServiceException extends Exception {
public ChatServiceException(String message) {
super(message);
}
}

View File

@ -0,0 +1,13 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net;
/** The order of values in this enum should match {@code IpType} enum in Rust (libsignal-net). */
public enum IpType {
UNKNOWN,
IPv4,
IPv6
}

View File

@ -20,11 +20,15 @@ public class Network {
private final int value;
private Environment(int value) {
Environment(int value) {
this.value = value;
}
}
private final TokioAsyncContext tokioAsyncContext;
private final ConnectionManager connectionManager;
/**
* Group of the APIs responsible for communication with the SVR3 service.
*
@ -65,25 +69,18 @@ public class Network {
return this.connectionManager;
}
class ConnectionManager implements NativeHandleGuard.Owner {
private long nativeHandle;
public ChatService createChatService(final String username, final String password) {
return new ChatService(tokioAsyncContext, connectionManager, username, password);
}
static class ConnectionManager extends NativeHandleGuard.SimpleOwner {
private ConnectionManager(Environment env) {
this.nativeHandle = Native.ConnectionManager_new(env.value);
super(Native.ConnectionManager_new(env.value));
}
@Override
public long unsafeNativeHandleWithoutGuard() {
return this.nativeHandle;
}
@Override
@SuppressWarnings("deprecation")
protected void finalize() {
Native.ConnectionManager_Destroy(this.nativeHandle);
protected void release(final long nativeHandle) {
Native.ConnectionManager_Destroy(nativeHandle);
}
}
private TokioAsyncContext tokioAsyncContext;
private ConnectionManager connectionManager;
}

View File

@ -8,21 +8,13 @@ package org.signal.libsignal.net;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
class TokioAsyncContext implements NativeHandleGuard.Owner {
private long nativeHandle;
class TokioAsyncContext extends NativeHandleGuard.SimpleOwner {
TokioAsyncContext() {
this.nativeHandle = Native.TokioAsyncContext_new();
super(Native.TokioAsyncContext_new());
}
@Override
public long unsafeNativeHandleWithoutGuard() {
return this.nativeHandle;
}
@Override
@SuppressWarnings("deprecation")
protected void finalize() {
Native.TokioAsyncContext_Destroy(this.nativeHandle);
protected void release(final long nativeHandle) {
Native.TokioAsyncContext_Destroy(nativeHandle);
}
}

View File

@ -0,0 +1,94 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net;
import static org.junit.Assert.*;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import org.junit.Test;
import org.signal.libsignal.internal.Native;
public class ChatServiceTest {
private static final int EXPECTED_STATUS = 200;
private static final String EXPECTED_MESSAGE = "OK";
private static final byte[] EXPECTED_CONTENT = "content".getBytes(StandardCharsets.UTF_8);
private static final Map<String, String> EXPECTED_HEADERS =
Map.of(
"user-agent", "test",
"forwarded", "1.1.1.1");
@Test
public void testConvertResponse() throws Exception {
// empty body
final ChatService.Response response1 =
(ChatService.Response) Native.TESTING_ChatServiceResponseConvert(false);
assertEquals(EXPECTED_STATUS, response1.status());
assertEquals(EXPECTED_MESSAGE, response1.message());
assertArrayEquals(new byte[0], response1.body());
assertEquals(EXPECTED_HEADERS, response1.headers());
final ChatService.Response response2 =
(ChatService.Response) Native.TESTING_ChatServiceResponseConvert(true);
assertEquals(EXPECTED_STATUS, response2.status());
assertEquals(EXPECTED_MESSAGE, response2.message());
assertArrayEquals(EXPECTED_CONTENT, response2.body());
assertEquals(EXPECTED_HEADERS, response2.headers());
}
@Test
public void testConvertDebugInfo() throws Exception {
final ChatService.DebugInfo debugInfo =
(ChatService.DebugInfo) Native.TESTING_ChatServiceDebugInfoConvert();
assertTrue(debugInfo.connectionReused());
assertEquals(2, debugInfo.reconnectCount());
assertEquals(IpType.IPv4, debugInfo.ipType());
}
@Test
public void testConvertResponseAndDebugInfo() throws Exception {
final ChatService.ResponseAndDebugInfo responseAndDebugInfo =
(ChatService.ResponseAndDebugInfo) Native.TESTING_ChatServiceResponseAndDebugInfoConvert();
final ChatService.Response response = responseAndDebugInfo.response();
assertEquals(EXPECTED_STATUS, response.status());
assertEquals(EXPECTED_MESSAGE, response.message());
assertArrayEquals(EXPECTED_CONTENT, response.body());
assertEquals(EXPECTED_HEADERS, response.headers());
final ChatService.DebugInfo debugInfo = responseAndDebugInfo.debugInfo();
assertTrue(debugInfo.connectionReused());
assertEquals(2, debugInfo.reconnectCount());
assertEquals(IpType.IPv4, debugInfo.ipType());
}
@Test(expected = ChatServiceException.class)
public void testConvertError() throws Exception {
Native.TESTING_ChatServiceErrorConvert();
}
@Test
public void testConstructRequest() throws Exception {
final String expectedMethod = "GET";
final String expectedPathAndQuery = "/test";
final ChatService.Request request =
new ChatService.Request(
expectedMethod, expectedPathAndQuery, EXPECTED_HEADERS, EXPECTED_CONTENT, 5000);
final ChatService.InternalRequest internal = ChatService.buildInternalRequest(request);
assertEquals(expectedMethod, internal.guardedMap(Native::TESTING_ChatRequestGetMethod));
assertEquals(expectedPathAndQuery, internal.guardedMap(Native::TESTING_ChatRequestGetPath));
assertArrayEquals(EXPECTED_CONTENT, internal.guardedMap(Native::TESTING_ChatRequestGetBody));
EXPECTED_HEADERS.forEach(
(name, value) ->
assertEquals(
value,
internal.guardedMap(h -> Native.TESTING_ChatRequestGetHeaderValue(h, name))));
}
}

View File

@ -8,7 +8,7 @@ pluginManagement {
rootProject.name = 'libsignal'
include ':client', ':server', ':shared'
include 'client', 'server', 'shared'
if (hasProperty('skipAndroid')) {
// Do nothing

View File

@ -24,25 +24,47 @@ import org.signal.libsignal.protocol.logging.Log;
*/
public class FilterExceptions {
/** A "functional interface" for an operation that returns an object and can throw. */
public static interface ThrowingNativeOperation<R> {
@FunctionalInterface
public interface ThrowingNativeOperation<R> {
R run() throws Exception;
}
/** A "functional interface" for an operation that returns an {@code int} and can throw. */
public static interface ThrowingNativeIntOperation {
@FunctionalInterface
public interface ThrowingNativeIntOperation {
int run() throws Exception;
}
/** A "functional interface" for an operation that returns a {@code long} and can throw. */
public static interface ThrowingNativeLongOperation {
@FunctionalInterface
public interface ThrowingNativeLongOperation {
long run() throws Exception;
}
/** A "functional interface" for an operation that has no result but can throw. */
public static interface ThrowingNativeVoidOperation {
@FunctionalInterface
public interface ThrowingNativeVoidOperation {
void run() throws Exception;
}
/**
* A "functional interface" for an operation that maps a {@code long} value to an object and can
* throw.
*/
@FunctionalInterface
public interface ThrowingLongFunction<R> {
R apply(long value) throws Exception;
}
/**
* A "functional interface" for an operation that accepts a {@code long} value, has no result, and
* can throw.
*/
@FunctionalInterface
public interface ThrowingLongConsumer {
void accept(long value) throws Exception;
}
private static AssertionError reportUnexpectedException(Exception e) {
Log.e("libsignal", "Unexpected checked exception " + e.getClass(), e);
return new AssertionError(e);

View File

@ -161,6 +161,11 @@ public final class Native {
public static native CompletableFuture<Long> CdsiLookup_new(long asyncRuntime, long connectionManager, String username, String password, long request, int timeoutMillis);
public static native byte[] CdsiLookup_token(long lookup);
public static native CompletableFuture ChatService_disconnect(long asyncRuntime, long chat);
public static native long ChatService_new(long connectionManager, String username, String password);
public static native CompletableFuture<Object> ChatService_unauth_send(long asyncRuntime, long chat, long httpRequest, int timeoutMillis);
public static native CompletableFuture<Object> ChatService_unauth_send_and_debug(long asyncRuntime, long chat, long httpRequest, int timeoutMillis);
public static native void Chat_Destroy(long handle);
public static native void ConnectionManager_Destroy(long handle);
@ -291,6 +296,8 @@ public final class Native {
public static native long HsmEnclaveClient_New(byte[] trustedPublicKey, byte[] trustedCodeHashes) throws Exception;
public static native void HttpRequest_Destroy(long handle);
public static native void HttpRequest_add_header(long request, String name, String value);
public static native long HttpRequest_new(String method, String path, byte[] bodyAsSlice) throws Exception;
public static native long[] IdentityKeyPair_Deserialize(byte[] data);
public static native byte[] IdentityKeyPair_Serialize(long publicKey, long privateKey);
@ -595,6 +602,14 @@ public final class Native {
public static native void TESTING_CdsiLookupErrorConvert() throws Exception;
public static native CompletableFuture<Object> TESTING_CdsiLookupResponseConvert(long asyncRuntime);
public static native byte[] TESTING_ChatRequestGetBody(long request);
public static native String TESTING_ChatRequestGetHeaderValue(long request, String headerName);
public static native String TESTING_ChatRequestGetMethod(long request);
public static native String TESTING_ChatRequestGetPath(long request);
public static native Object TESTING_ChatServiceDebugInfoConvert() throws Exception;
public static native void TESTING_ChatServiceErrorConvert() throws Exception;
public static native Object TESTING_ChatServiceResponseAndDebugInfoConvert() throws Exception;
public static native Object TESTING_ChatServiceResponseConvert(boolean bodyPresent) throws Exception;
public static native void TESTING_ErrorOnBorrowAsync(Object input);
public static native CompletableFuture TESTING_ErrorOnBorrowIo(long asyncRuntime, Object input);
public static native void TESTING_ErrorOnBorrowSync(Object input);

View File

@ -5,6 +5,9 @@
package org.signal.libsignal.internal;
import java.util.function.LongConsumer;
import java.util.function.LongFunction;
/**
* Provides access to a Rust object handle while keeping the Java wrapper alive.
*
@ -21,10 +24,58 @@ public class NativeHandleGuard implements AutoCloseable {
/**
* @see NativeHandleGuard
*/
public static interface Owner {
public interface Owner {
long unsafeNativeHandleWithoutGuard();
}
public abstract static class SimpleOwner implements Owner {
private final long nativeHandle;
protected SimpleOwner(final long nativeHandle) {
this.nativeHandle = nativeHandle;
}
protected abstract void release(long nativeHandle);
@Override
public long unsafeNativeHandleWithoutGuard() {
return nativeHandle;
}
@Override
@SuppressWarnings("deprecation")
protected void finalize() {
release(this.nativeHandle);
}
public <T> T guardedMap(final LongFunction<T> function) {
try (final NativeHandleGuard guard = new NativeHandleGuard(this)) {
return function.apply(guard.nativeHandle());
}
}
public <T> T guardedMapChecked(final FilterExceptions.ThrowingLongFunction<T> function)
throws Exception {
try (final NativeHandleGuard guard = new NativeHandleGuard(this)) {
return function.apply(guard.nativeHandle());
}
}
public void guardedRun(final LongConsumer consumer) {
try (final NativeHandleGuard guard = new NativeHandleGuard(this)) {
consumer.accept(guard.nativeHandle());
}
}
public void guardedRunChecked(final FilterExceptions.ThrowingLongConsumer consumer)
throws Exception {
try (final NativeHandleGuard guard = new NativeHandleGuard(this)) {
consumer.accept(guard.nativeHandle());
}
}
}
private final Owner owner;
public NativeHandleGuard(Owner owner) {

1
node/Native.d.ts vendored
View File

@ -467,6 +467,7 @@ export function TESTING_ChatRequestGetMethod(request: Wrapper<HttpRequest>): str
export function TESTING_ChatRequestGetPath(request: Wrapper<HttpRequest>): string;
export function TESTING_ChatServiceDebugInfoConvert(): DebugInfo;
export function TESTING_ChatServiceErrorConvert(): void;
export function TESTING_ChatServiceResponseAndDebugInfoConvert(): ResponseAndDebugInfo;
export function TESTING_ChatServiceResponseConvert(bodyPresent: boolean): Response;
export function TESTING_ErrorOnBorrowAsync(_input: null): Promise<void>;
export function TESTING_ErrorOnBorrowIo(asyncRuntime: Wrapper<NonSuspendingBackgroundThreadRuntime>, _input: null): Promise<void>;

View File

@ -33,13 +33,13 @@ describe('chat service api', () => {
];
const expectedWithContent: Response = {
status: status,
message: undefined,
message: 'OK',
headers: headers,
body: Buffer.from('content'),
};
const expectedWithoutContent: Response = {
status: status,
message: undefined,
message: 'OK',
headers: headers,
body: undefined,
};

View File

@ -16,6 +16,7 @@ use std::ops::Deref;
use crate::io::{InputStream, SyncInputStream};
use crate::message_backup::MessageBackupValidationOutcome;
use crate::net::ResponseAndDebugInfo;
use crate::support::{Array, AsType, FixedLengthBincodeSerializable, Serialized};
use super::*;
@ -1021,6 +1022,125 @@ impl<'a> ResultTypeInfo<'a> for libsignal_net::cdsi::LookupResponse {
}
}
impl<'a> ResultTypeInfo<'a> for libsignal_net::chat::Response {
type ResultType = JObject<'a>;
fn convert_into(self, env: &mut JNIEnv<'a>) -> Result<Self::ResultType, BridgeLayerError> {
let Self {
status,
message,
body,
headers,
} = self;
// body
let body = body.as_deref().unwrap_or(&[]);
let body_arr = env.byte_array_from_slice(body)?;
// message
let message_local = env.new_string(message.as_deref().unwrap_or(""))?;
// headers
let headers_map = new_object(
env,
jni_class_name!(java.util.HashMap),
jni_args!(() -> void),
)?;
let headers_jmap = JMap::from_env(env, &headers_map)?;
for (name, value) in headers.iter() {
let name_str = env.new_string(name.as_str())?;
let value_str = env.new_string(value.to_str().expect("valid header value"))?;
headers_jmap.put(env, &name_str, &value_str)?;
}
let class = {
const RESPONSE_CLASS: &str =
jni_class_name!(org.signal.libsignal.net.ChatService::Response);
get_preloaded_class(env, RESPONSE_CLASS)
.transpose()
.unwrap_or_else(|| env.find_class(RESPONSE_CLASS))?
};
Ok(new_object(
env,
class,
jni_args!((
status.as_u16().into() => int,
message_local => java.lang.String,
headers_jmap => java.util.Map,
body_arr => [byte]
) -> void),
)?)
}
}
impl<'a> ResultTypeInfo<'a> for libsignal_net::chat::DebugInfo {
type ResultType = JObject<'a>;
fn convert_into(self, env: &mut JNIEnv<'a>) -> Result<Self::ResultType, BridgeLayerError> {
let Self {
connection_reused,
reconnect_count,
ip_type,
} = self;
// reconnect count as i32
let reconnect_count_i32: i32 = reconnect_count.try_into().expect("within i32 range");
// ip type as code
let ip_type_byte = ip_type as i8;
let class = {
const RESPONSE_CLASS: &str =
jni_class_name!(org.signal.libsignal.net.ChatService::DebugInfo);
get_preloaded_class(env, RESPONSE_CLASS)
.transpose()
.unwrap_or_else(|| env.find_class(RESPONSE_CLASS))?
};
Ok(new_object(
env,
class,
jni_args!((
connection_reused => boolean,
reconnect_count_i32 => int,
ip_type_byte => byte
) -> void),
)?)
}
}
impl<'a> ResultTypeInfo<'a> for ResponseAndDebugInfo {
type ResultType = JObject<'a>;
fn convert_into(self, env: &mut JNIEnv<'a>) -> Result<Self::ResultType, BridgeLayerError> {
let Self {
response,
debug_info,
} = self;
let response: JObject<'a> = response.convert_into(env)?;
let debug_info: JObject<'a> = debug_info.convert_into(env)?;
let class = {
const RESPONSE_CLASS: &str =
jni_class_name!(org.signal.libsignal.net.ChatService::ResponseAndDebugInfo);
get_preloaded_class(env, RESPONSE_CLASS)
.transpose()
.unwrap_or_else(|| env.find_class(RESPONSE_CLASS))?
};
Ok(new_object(
env,
class,
jni_args!((
response => org.signal.libsignal.net.ChatService::Response,
debug_info => org.signal.libsignal.net.ChatService::DebugInfo
) -> void),
)?)
}
}
/// Converts each element of `it` to a Java object, storing the result in an array.
///
/// `element_type_signature` should use [`jni_class_name`] if it's a plain class and
@ -1344,6 +1464,15 @@ macro_rules! jni_result_type {
(LookupResponse) => {
jni::JObject<'local>
};
(Response) => {
jni::JObject<'local>
};
(DebugInfo) => {
jni::JObject<'local>
};
(ResponseAndDebugInfo) => {
jni::JObject<'local>
};
(CiphertextMessage) => {
jni::JavaCiphertextMessage<'local>
};

View File

@ -2,6 +2,7 @@
// Copyright 2020-2021 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use http::uri::InvalidUri;
use std::fmt;
use std::io::{Error as IoError, ErrorKind as IoErrorKind};
use std::time::Duration;
@ -11,6 +12,7 @@ use jni::{JNIEnv, JavaVM};
use attest::hsm_enclave::Error as HsmEnclaveError;
use device_transfer::Error as DeviceTransferError;
use libsignal_net::chat::ChatServiceError;
use libsignal_net::infra::ws::{WebSocketConnectError, WebSocketServiceError};
use libsignal_protocol::*;
use signal_crypto::Error as SignalCryptoError;
@ -45,6 +47,8 @@ pub enum SignalJniError {
Cdsi(CdsiError),
Svr3(libsignal_net::svr3::Error),
WebSocket(#[from] WebSocketServiceError),
ChatService(ChatServiceError),
InvalidUri(InvalidUri),
Timeout,
Bridge(BridgeLayerError),
#[cfg(feature = "testing-fns")]
@ -90,6 +94,8 @@ impl fmt::Display for SignalJniError {
#[cfg(feature = "signal-media")]
SignalJniError::WebpSanitizeParse(e) => write!(f, "{}", e),
SignalJniError::Cdsi(e) => write!(f, "{}", e),
SignalJniError::ChatService(e) => write!(f, "{}", e),
SignalJniError::InvalidUri(e) => write!(f, "{}", e),
SignalJniError::WebSocket(e) => write!(f, "{e}"),
SignalJniError::Timeout => write!(f, "timeout"),
SignalJniError::Svr3(e) => write!(f, "{}", e),
@ -201,6 +207,18 @@ impl From<UsernameLinkError> for SignalJniError {
}
}
impl From<InvalidUri> for SignalJniError {
fn from(e: InvalidUri) -> Self {
SignalJniError::InvalidUri(e)
}
}
impl From<ChatServiceError> for SignalJniError {
fn from(e: ChatServiceError) -> Self {
SignalJniError::ChatService(e)
}
}
impl From<IoError> for SignalJniError {
fn from(e: IoError) -> SignalJniError {
Self::Io(e)

View File

@ -556,6 +556,13 @@ where
}
SignalJniError::Svr3(_) => jni_class_name!(org.signal.libsignal.svr.SvrException),
SignalJniError::InvalidUri(_) => {
jni_class_name!(java.net.MalformedURLException)
}
SignalJniError::ChatService(_) => {
jni_class_name!(org.signal.libsignal.net.ChatServiceException)
}
#[cfg(feature = "testing-fns")]
SignalJniError::TestingError { exception_class } => exception_class,
};
@ -701,6 +708,10 @@ static PRELOADED_CLASSES: OnceCell<HashMap<&'static str, GlobalRef>> = OnceCell:
const PRELOADED_CLASS_NAMES: &[&str] = &[
jni_class_name!(org.signal.libsignal.net.CdsiLookupResponse::Entry),
jni_class_name!(org.signal.libsignal.net.CdsiLookupResponse),
jni_class_name!(org.signal.libsignal.net.ChatService),
jni_class_name!(org.signal.libsignal.net.ChatService::Response),
jni_class_name!(org.signal.libsignal.net.ChatService::DebugInfo),
jni_class_name!(org.signal.libsignal.net.ChatService::ResponseAndDebugInfo),
jni_class_name!(org.signal.libsignal.internal.TestingException),
];

View File

@ -257,11 +257,11 @@ pub struct ResponseAndDebugInfo {
bridge_handle!(Chat, clone = false);
bridge_handle!(HttpRequest, clone = false);
#[cfg(feature = "node")]
#[cfg(any(feature = "node", feature = "jni"))]
/// Newtype wrapper for implementing [`TryFrom`]`
struct HttpMethod(http::Method);
#[cfg(feature = "node")]
#[cfg(any(feature = "node", feature = "jni"))]
impl TryFrom<String> for HttpMethod {
type Error = <http::Method as FromStr>::Err;
fn try_from(value: String) -> Result<Self, Self::Error> {
@ -269,7 +269,7 @@ impl TryFrom<String> for HttpMethod {
}
}
#[bridge_fn(ffi = false, jni = false)]
#[bridge_fn(ffi = false)]
fn HttpRequest_new(
method: AsType<HttpMethod, String>,
path: String,
@ -286,7 +286,7 @@ fn HttpRequest_new(
})
}
#[bridge_fn_void(ffi = false, jni = false)]
#[bridge_fn_void(ffi = false)]
fn HttpRequest_add_header(
request: &HttpRequest,
name: AsType<HeaderName, String>,
@ -298,7 +298,7 @@ fn HttpRequest_add_header(
(*guard).append(header_key, header_value);
}
#[bridge_fn(ffi = false, jni = false)]
#[bridge_fn(ffi = false)]
fn ChatService_new(
connection_manager: &ConnectionManager,
username: String,
@ -317,12 +317,12 @@ fn ChatService_new(
}
}
#[bridge_io(TokioAsyncContext, ffi = false, jni = false)]
#[bridge_io(TokioAsyncContext, ffi = false)]
async fn ChatService_disconnect(chat: &Chat) {
chat.service.disconnect().await
}
#[bridge_io(TokioAsyncContext, ffi = false, jni = false)]
#[bridge_io(TokioAsyncContext, ffi = false)]
async fn ChatService_unauth_send(
chat: &Chat,
http_request: &HttpRequest,
@ -340,7 +340,7 @@ async fn ChatService_unauth_send(
.await
}
#[bridge_io(TokioAsyncContext, ffi = false, jni = false)]
#[bridge_io(TokioAsyncContext, ffi = false)]
async fn ChatService_unauth_send_and_debug(
chat: &Chat,
http_request: &HttpRequest,

View File

@ -11,7 +11,7 @@ use libsignal_protocol::{Aci, Pni};
use nonzero_ext::nonzero;
use uuid::Uuid;
use crate::net::{HttpRequest, TokioAsyncContext};
use crate::net::{HttpRequest, ResponseAndDebugInfo, TokioAsyncContext};
use crate::support::*;
use crate::*;
@ -48,12 +48,12 @@ fn TESTING_CdsiLookupErrorConvert() -> Result<(), LookupError> {
Err(LookupError::ParseError)
}
#[bridge_fn(ffi = false, jni = false)]
#[bridge_fn(ffi = false)]
fn TESTING_ChatServiceErrorConvert() -> Result<(), ChatServiceError> {
Err(ChatServiceError::Timeout)
}
#[bridge_fn(ffi = false, jni = false)]
#[bridge_fn(ffi = false)]
fn TESTING_ChatServiceResponseConvert(body_present: bool) -> Result<Response, ChatServiceError> {
let body = match body_present {
true => Some(b"content".to_vec().into_boxed_slice()),
@ -64,13 +64,13 @@ fn TESTING_ChatServiceResponseConvert(body_present: bool) -> Result<Response, Ch
headers.append(http::header::FORWARDED, HeaderValue::from_static("1.1.1.1"));
Ok(Response {
status: StatusCode::OK,
message: None,
message: Some("OK".to_string()),
body,
headers,
})
}
#[bridge_fn(ffi = false, jni = false)]
#[bridge_fn(ffi = false)]
fn TESTING_ChatServiceDebugInfoConvert() -> Result<DebugInfo, ChatServiceError> {
Ok(DebugInfo {
connection_reused: true,
@ -79,17 +79,26 @@ fn TESTING_ChatServiceDebugInfoConvert() -> Result<DebugInfo, ChatServiceError>
})
}
#[bridge_fn(ffi = false, jni = false)]
#[bridge_fn(ffi = false)]
fn TESTING_ChatServiceResponseAndDebugInfoConvert() -> Result<ResponseAndDebugInfo, ChatServiceError>
{
Ok(ResponseAndDebugInfo {
response: TESTING_ChatServiceResponseConvert(true)?,
debug_info: TESTING_ChatServiceDebugInfoConvert()?,
})
}
#[bridge_fn(ffi = false)]
fn TESTING_ChatRequestGetMethod(request: &HttpRequest) -> String {
request.method.to_string()
}
#[bridge_fn(ffi = false, jni = false)]
#[bridge_fn(ffi = false)]
fn TESTING_ChatRequestGetPath(request: &HttpRequest) -> String {
request.path.to_string()
}
#[bridge_fn(ffi = false, jni = false)]
#[bridge_fn(ffi = false)]
fn TESTING_ChatRequestGetHeaderValue(request: &HttpRequest, header_name: String) -> String {
request
.headers
@ -102,7 +111,7 @@ fn TESTING_ChatRequestGetHeaderValue(request: &HttpRequest, header_name: String)
.to_string()
}
#[bridge_fn(ffi = false, jni = false)]
#[bridge_fn(ffi = false)]
fn TESTING_ChatRequestGetBody(request: &HttpRequest) -> Option<Vec<u8>> {
request.body.clone().map(|b| b.to_vec())
}