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:
parent
23764a50e8
commit
d7a4b8c817
7
java/.gitignore
vendored
Normal file
7
java/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
# IntelliJ specific files/directories
|
||||
out
|
||||
.shelf
|
||||
.idea
|
||||
*.ipr
|
||||
*.iws
|
||||
*.iml
|
@ -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) {}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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))));
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ pluginManagement {
|
||||
|
||||
rootProject.name = 'libsignal'
|
||||
|
||||
include ':client', ':server', ':shared'
|
||||
include 'client', 'server', 'shared'
|
||||
|
||||
if (hasProperty('skipAndroid')) {
|
||||
// Do nothing
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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
1
node/Native.d.ts
vendored
@ -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>;
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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>
|
||||
};
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
];
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user