mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-27 04:04:43 +01:00
Convert device linking apis to use websockets.
This commit is contained in:
committed by
Michelle Tang
parent
451d12ed53
commit
c38342e2fb
@@ -5,15 +5,21 @@
|
||||
|
||||
package org.whispersystems.signalservice.api
|
||||
|
||||
import org.signal.core.util.concurrent.safeBlockingGet
|
||||
import org.whispersystems.signalservice.api.NetworkResult.StatusCodeError
|
||||
import org.whispersystems.signalservice.api.NetworkResult.Success
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
|
||||
import org.whispersystems.signalservice.internal.websocket.WebsocketResponse
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeoutException
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.cast
|
||||
import kotlin.time.Duration
|
||||
|
||||
typealias StatusCodeErrorAction = (NetworkResult.StatusCodeError<*>) -> Unit
|
||||
|
||||
@@ -51,49 +57,64 @@ sealed class NetworkResult<T>(
|
||||
ApplicationError(e)
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience method to convert a websocket request into a network result.
|
||||
* Common HTTP errors will be translated to [StatusCodeError]s.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun fromWebSocketRequest(
|
||||
signalWebSocket: SignalWebSocket,
|
||||
request: WebSocketRequestMessage
|
||||
): NetworkResult<Unit> = fromWebSocketRequest(
|
||||
signalWebSocket = signalWebSocket,
|
||||
request = request,
|
||||
clazz = Unit::class
|
||||
)
|
||||
|
||||
/**
|
||||
* A convenience method to convert a websocket request into a network result with simple conversion of the response body to the desired class.
|
||||
* Common exceptions will be caught and translated to errors.
|
||||
* Common HTTP errors will be translated to [StatusCodeError]s.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <T : Any> fromWebSocketRequest(
|
||||
signalWebSocket: SignalWebSocket,
|
||||
request: WebSocketRequestMessage,
|
||||
clazz: KClass<T>
|
||||
clazz: KClass<T>,
|
||||
timeout: Duration = WebSocketConnection.DEFAULT_SEND_TIMEOUT
|
||||
): NetworkResult<T> {
|
||||
return fromWebSocketRequest(
|
||||
signalWebSocket = signalWebSocket,
|
||||
request = request,
|
||||
timeout = timeout,
|
||||
webSocketResponseConverter = DefaultWebSocketConverter(clazz)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience method to convert a websocket request into a network result with the ability to fully customize the conversion of the response.
|
||||
* Common HTTP errors will be translated to [StatusCodeError]s.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <T : Any> fromWebSocketRequest(
|
||||
signalWebSocket: SignalWebSocket,
|
||||
request: WebSocketRequestMessage,
|
||||
timeout: Duration = WebSocketConnection.DEFAULT_SEND_TIMEOUT,
|
||||
webSocketResponseConverter: WebSocketResponseConverter<T>
|
||||
): NetworkResult<T> = try {
|
||||
val result: Result<T> = signalWebSocket.request(request)
|
||||
.map { response: WebsocketResponse -> Result.success(JsonUtil.fromJson(response.body, clazz.java)) }
|
||||
.onErrorReturn { Result.failure<T>(it) }
|
||||
.blockingGet()
|
||||
Success(result.getOrThrow())
|
||||
val result: Result<NetworkResult<T>> = signalWebSocket.request(request, timeout)
|
||||
.map { response: WebsocketResponse -> Result.success(webSocketResponseConverter.convert(response)) }
|
||||
.onErrorReturn { Result.failure(it) }
|
||||
.safeBlockingGet()
|
||||
|
||||
result.getOrThrow()
|
||||
} catch (e: NonSuccessfulResponseCodeException) {
|
||||
StatusCodeError(e)
|
||||
} catch (e: IOException) {
|
||||
NetworkError(e)
|
||||
} catch (e: TimeoutException) {
|
||||
NetworkError(PushNetworkException(e))
|
||||
} catch (e: Throwable) {
|
||||
ApplicationError(e)
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience method to convert a websocket request into a network result with the ability to convert the response to your target class.
|
||||
* Common exceptions will be caught and translated to errors.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <T : Any> fromWebSocketRequest(
|
||||
signalWebSocket: SignalWebSocket,
|
||||
request: WebSocketRequestMessage,
|
||||
webSocketResponseConverter: WebSocketResponseConverter<T>
|
||||
): NetworkResult<T> = try {
|
||||
val result = signalWebSocket.request(request)
|
||||
.map { response: WebsocketResponse -> webSocketResponseConverter.convert(response) }
|
||||
.blockingGet()
|
||||
Success(result)
|
||||
} catch (e: NonSuccessfulResponseCodeException) {
|
||||
StatusCodeError(e)
|
||||
} catch (e: IOException) {
|
||||
NetworkError(e)
|
||||
} catch (e: InterruptedException) {
|
||||
NetworkError(PushNetworkException(e))
|
||||
} catch (e: Throwable) {
|
||||
ApplicationError(e)
|
||||
}
|
||||
@@ -308,6 +329,37 @@ sealed class NetworkResult<T>(
|
||||
|
||||
fun interface WebSocketResponseConverter<T> {
|
||||
@Throws(Exception::class)
|
||||
fun convert(response: WebsocketResponse): T
|
||||
fun convert(response: WebsocketResponse): NetworkResult<T>
|
||||
}
|
||||
|
||||
class DefaultWebSocketConverter<T : Any>(private val responseJsonClass: KClass<T>) : WebSocketResponseConverter<T> {
|
||||
override fun convert(response: WebsocketResponse): NetworkResult<T> {
|
||||
return if (response.status < 200 || response.status > 299) {
|
||||
response.toStatusCodeError()
|
||||
} else {
|
||||
response.toSuccess(responseJsonClass)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LongPollingWebSocketConverter<T : Any>(private val responseJsonClass: KClass<T>) : WebSocketResponseConverter<T> {
|
||||
override fun convert(response: WebsocketResponse): NetworkResult<T> {
|
||||
return if (response.status == 204 || response.status < 200 || response.status > 299) {
|
||||
response.toStatusCodeError()
|
||||
} else {
|
||||
response.toSuccess(responseJsonClass)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Any> WebsocketResponse.toStatusCodeError(): NetworkResult<T> {
|
||||
return StatusCodeError(NonSuccessfulResponseCodeException(this.status, "", this.body))
|
||||
}
|
||||
|
||||
private fun <T : Any> WebsocketResponse.toSuccess(responseJsonClass: KClass<T>): NetworkResult<T> {
|
||||
if (responseJsonClass == Unit::class) {
|
||||
return Success(responseJsonClass.cast(Unit))
|
||||
}
|
||||
return Success(JsonUtil.fromJson(this.body, responseJsonClass.java))
|
||||
}
|
||||
|
||||
@@ -297,15 +297,6 @@ public class SignalServiceAccountManager {
|
||||
return pushServiceSocket.getAccountDataReport();
|
||||
}
|
||||
|
||||
|
||||
public List<DeviceInfo> getDevices() throws IOException {
|
||||
return this.pushServiceSocket.getDevices();
|
||||
}
|
||||
|
||||
public void removeDevice(int deviceId) throws IOException {
|
||||
this.pushServiceSocket.removeDevice(deviceId);
|
||||
}
|
||||
|
||||
public List<TurnServerInfo> getTurnServerInfo() throws IOException {
|
||||
List<TurnServerInfo> relays = this.pushServiceSocket.getCallingRelays().getRelays();
|
||||
return relays != null ? relays : Collections.emptyList();
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
package org.whispersystems.signalservice.api.link
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.Base64.encodeWithPadding
|
||||
import org.signal.core.util.urlEncode
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
@@ -13,18 +15,54 @@ import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey
|
||||
import org.whispersystems.signalservice.api.backup.MessageBackupKey
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
|
||||
import org.whispersystems.signalservice.internal.crypto.PrimaryProvisioningCipher
|
||||
import org.whispersystems.signalservice.internal.delete
|
||||
import org.whispersystems.signalservice.internal.get
|
||||
import org.whispersystems.signalservice.internal.push.DeviceInfoList
|
||||
import org.whispersystems.signalservice.internal.push.ProvisionMessage
|
||||
import org.whispersystems.signalservice.internal.push.ProvisioningMessage
|
||||
import org.whispersystems.signalservice.internal.push.ProvisioningVersion
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
import kotlin.math.min
|
||||
import org.whispersystems.signalservice.internal.put
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Class to interact with device-linking endpoints.
|
||||
*/
|
||||
class LinkDeviceApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
class LinkDeviceApi(
|
||||
private val authWebSocket: SignalWebSocket.AuthenticatedWebSocket
|
||||
) {
|
||||
/**
|
||||
* Fetches a list of linked devices.
|
||||
*
|
||||
* GET /v1/devices
|
||||
*
|
||||
* - 200: Success
|
||||
*/
|
||||
fun getDevices(): NetworkResult<List<DeviceInfo>> {
|
||||
val request = WebSocketRequestMessage.get("/v1/devices")
|
||||
return NetworkResult
|
||||
.fromWebSocketRequest(authWebSocket, request, DeviceInfoList::class)
|
||||
.map { it.getDevices() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove and unlink a linked device.
|
||||
*
|
||||
* DELETE /v1/devices/{id}
|
||||
*
|
||||
* - 200: Success
|
||||
*/
|
||||
fun removeDevice(deviceId: Int): NetworkResult<Unit> {
|
||||
val request = WebSocketRequestMessage.delete("/v1/devices/$deviceId")
|
||||
return NetworkResult
|
||||
.fromWebSocketRequest(authWebSocket, request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a new verification code that lets you link a new device.
|
||||
@@ -36,15 +74,15 @@ class LinkDeviceApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
* - 429: Rate-limited.
|
||||
*/
|
||||
fun getDeviceVerificationCode(): NetworkResult<LinkedDeviceVerificationCodeResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
pushServiceSocket.getLinkedDeviceVerificationCode()
|
||||
}
|
||||
val request = WebSocketRequestMessage.get("/v1/devices/provisioning/code")
|
||||
return NetworkResult
|
||||
.fromWebSocketRequest(authWebSocket, request, LinkedDeviceVerificationCodeResponse::class)
|
||||
}
|
||||
|
||||
/**
|
||||
* Links a new device to the account.
|
||||
*
|
||||
* PUT /v1/devices/link
|
||||
* PUT /v1/provisioning/[deviceIdentifier]
|
||||
*
|
||||
* - 200: Success.
|
||||
* - 403: Account not found or incorrect verification code.
|
||||
@@ -67,45 +105,50 @@ class LinkDeviceApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
code: String,
|
||||
ephemeralMessageBackupKey: MessageBackupKey?
|
||||
): NetworkResult<Unit> {
|
||||
return NetworkResult.fromFetch {
|
||||
val cipher = PrimaryProvisioningCipher(deviceKey)
|
||||
val message = ProvisionMessage(
|
||||
aciIdentityKeyPublic = aciIdentityKeyPair.publicKey.serialize().toByteString(),
|
||||
aciIdentityKeyPrivate = aciIdentityKeyPair.privateKey.serialize().toByteString(),
|
||||
pniIdentityKeyPublic = pniIdentityKeyPair.publicKey.serialize().toByteString(),
|
||||
pniIdentityKeyPrivate = pniIdentityKeyPair.privateKey.serialize().toByteString(),
|
||||
aci = aci.toString(),
|
||||
pni = pni.toStringWithoutPrefix(),
|
||||
number = e164,
|
||||
profileKey = profileKey.serialize().toByteString(),
|
||||
provisioningCode = code,
|
||||
provisioningVersion = ProvisioningVersion.CURRENT.value,
|
||||
masterKey = masterKey.serialize().toByteString(),
|
||||
mediaRootBackupKey = mediaRootBackupKey.value.toByteString(),
|
||||
ephemeralBackupKey = ephemeralMessageBackupKey?.value?.toByteString()
|
||||
)
|
||||
val ciphertext = cipher.encrypt(message)
|
||||
val cipher = PrimaryProvisioningCipher(deviceKey)
|
||||
val message = ProvisionMessage(
|
||||
aciIdentityKeyPublic = aciIdentityKeyPair.publicKey.serialize().toByteString(),
|
||||
aciIdentityKeyPrivate = aciIdentityKeyPair.privateKey.serialize().toByteString(),
|
||||
pniIdentityKeyPublic = pniIdentityKeyPair.publicKey.serialize().toByteString(),
|
||||
pniIdentityKeyPrivate = pniIdentityKeyPair.privateKey.serialize().toByteString(),
|
||||
aci = aci.toString(),
|
||||
pni = pni.toStringWithoutPrefix(),
|
||||
number = e164,
|
||||
profileKey = profileKey.serialize().toByteString(),
|
||||
provisioningCode = code,
|
||||
provisioningVersion = ProvisioningVersion.CURRENT.value,
|
||||
masterKey = masterKey.serialize().toByteString(),
|
||||
mediaRootBackupKey = mediaRootBackupKey.value.toByteString(),
|
||||
ephemeralBackupKey = ephemeralMessageBackupKey?.value?.toByteString()
|
||||
)
|
||||
val ciphertext: ByteArray = cipher.encrypt(message)
|
||||
val body = ProvisioningMessage(encodeWithPadding(ciphertext))
|
||||
|
||||
pushServiceSocket.sendProvisioningMessage(deviceIdentifier, ciphertext)
|
||||
}
|
||||
val request = WebSocketRequestMessage.put("/v1/provisioning/${deviceIdentifier.urlEncode()}", body)
|
||||
return NetworkResult.fromWebSocketRequest(authWebSocket, request)
|
||||
}
|
||||
|
||||
/**
|
||||
* A "long-polling" endpoint that will return once the device has successfully been linked.
|
||||
*
|
||||
* @param timeoutSeconds The max amount of time to wait. Capped at 30 seconds.
|
||||
* @param timeout The max amount of time to wait. Capped at 30 seconds.
|
||||
*
|
||||
* GET /v1/devices/wait_for_linked_device/{token}
|
||||
* GET /v1/devices/wait_for_linked_device/[token]?timeout=[timeout]
|
||||
*
|
||||
* - 200: Success, a new device was linked associated with the provided token.
|
||||
* - 204: No device was linked before the max waiting time elapsed.
|
||||
* - 400: Invalid token/timeout.
|
||||
* - 429: Rate-limited.
|
||||
*/
|
||||
fun waitForLinkedDevice(token: String, timeoutSeconds: Int = 30): NetworkResult<WaitForLinkedDeviceResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
pushServiceSocket.waitForLinkedDevice(token, min(timeoutSeconds, 30))
|
||||
}
|
||||
fun waitForLinkedDevice(token: String, timeout: Duration = 30.seconds): NetworkResult<WaitForLinkedDeviceResponse> {
|
||||
val request = WebSocketRequestMessage.get("/v1/devices/wait_for_linked_device/${token.urlEncode()}?timeout=${timeout.inWholeSeconds}")
|
||||
return NetworkResult
|
||||
.fromWebSocketRequest(
|
||||
signalWebSocket = authWebSocket,
|
||||
request = request,
|
||||
timeout = timeout,
|
||||
webSocketResponseConverter = NetworkResult.LongPollingWebSocketConverter(WaitForLinkedDeviceResponse::class)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,18 +161,16 @@ class LinkDeviceApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
* - 429: Rate-limited.
|
||||
*/
|
||||
fun setTransferArchive(destinationDeviceId: Int, destinationDeviceCreated: Long, cdn: Int, cdnKey: String): NetworkResult<Unit> {
|
||||
return NetworkResult.fromFetch {
|
||||
pushServiceSocket.setLinkedDeviceTransferArchive(
|
||||
SetLinkedDeviceTransferArchiveRequest(
|
||||
destinationDeviceId = destinationDeviceId,
|
||||
destinationDeviceCreated = destinationDeviceCreated,
|
||||
transferArchive = SetLinkedDeviceTransferArchiveRequest.TransferArchive.CdnInfo(
|
||||
cdn = cdn,
|
||||
key = cdnKey
|
||||
)
|
||||
)
|
||||
val body = SetLinkedDeviceTransferArchiveRequest(
|
||||
destinationDeviceId = destinationDeviceId,
|
||||
destinationDeviceCreated = destinationDeviceCreated,
|
||||
transferArchive = SetLinkedDeviceTransferArchiveRequest.TransferArchive.CdnInfo(
|
||||
cdn = cdn,
|
||||
key = cdnKey
|
||||
)
|
||||
}
|
||||
)
|
||||
val request = WebSocketRequestMessage.put("/v1/devices/transfer_archive", body)
|
||||
return NetworkResult.fromWebSocketRequest(authWebSocket, request)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,31 +184,26 @@ class LinkDeviceApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
* - 429: Rate-limited.
|
||||
*/
|
||||
fun setTransferArchiveError(destinationDeviceId: Int, destinationDeviceCreated: Long, error: TransferArchiveError): NetworkResult<Unit> {
|
||||
return NetworkResult.fromFetch {
|
||||
pushServiceSocket.setLinkedDeviceTransferArchive(
|
||||
SetLinkedDeviceTransferArchiveRequest(
|
||||
destinationDeviceId = destinationDeviceId,
|
||||
destinationDeviceCreated = destinationDeviceCreated,
|
||||
transferArchive = SetLinkedDeviceTransferArchiveRequest.TransferArchive.Error(
|
||||
error
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
val body = SetLinkedDeviceTransferArchiveRequest(
|
||||
destinationDeviceId = destinationDeviceId,
|
||||
destinationDeviceCreated = destinationDeviceCreated,
|
||||
transferArchive = SetLinkedDeviceTransferArchiveRequest.TransferArchive.Error(error)
|
||||
)
|
||||
val request = WebSocketRequestMessage.put("/v1/devices/transfer_archive", body)
|
||||
return NetworkResult.fromWebSocketRequest(authWebSocket, request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name for a linked device
|
||||
*
|
||||
* PUT /v1/accounts/name
|
||||
* PUT /v1/accounts/name?deviceId=[deviceId]
|
||||
*
|
||||
* - 204: Success.
|
||||
* - 403: Not authorized to change the name of the device with the given ID
|
||||
* - 404: No device found with the given ID
|
||||
*/
|
||||
fun setDeviceName(encryptedDeviceName: String, deviceId: Int): NetworkResult<Unit> {
|
||||
return NetworkResult.fromFetch {
|
||||
pushServiceSocket.setDeviceName(deviceId, SetDeviceNameRequest(encryptedDeviceName))
|
||||
}
|
||||
val request = WebSocketRequestMessage.put("/v1/accounts/name?deviceId=$deviceId", SetDeviceNameRequest(encryptedDeviceName))
|
||||
return NetworkResult.fromWebSocketRequest(authWebSocket, request)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.whispersystems.signalservice.internal.websocket.WebSocketResponseMess
|
||||
import org.whispersystems.signalservice.internal.websocket.WebsocketResponse
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeoutException
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* Base wrapper around a [WebSocketConnection] to provide a more developer friend interface to websocket
|
||||
@@ -100,6 +101,14 @@ sealed class SignalWebSocket(
|
||||
}
|
||||
}
|
||||
|
||||
fun request(request: WebSocketRequestMessage, timeout: Duration): Single<WebsocketResponse> {
|
||||
return try {
|
||||
getWebSocket().sendRequest(request, timeout.inWholeSeconds)
|
||||
} catch (e: IOException) {
|
||||
Single.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun sendAck(response: EnvelopeResponse) {
|
||||
getWebSocket().sendResponse(response.websocketRequest.getWebSocketResponse())
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.internal
|
||||
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
|
||||
import java.security.SecureRandom
|
||||
|
||||
/**
|
||||
* Create a basic GET web socket request
|
||||
*/
|
||||
fun WebSocketRequestMessage.Companion.get(path: String): WebSocketRequestMessage {
|
||||
return WebSocketRequestMessage(
|
||||
verb = "GET",
|
||||
path = path,
|
||||
id = SecureRandom().nextLong()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a basic DELETE web socket request
|
||||
*/
|
||||
fun WebSocketRequestMessage.Companion.delete(path: String): WebSocketRequestMessage {
|
||||
return WebSocketRequestMessage(
|
||||
verb = "DELETE",
|
||||
path = path,
|
||||
id = SecureRandom().nextLong()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a basic PUT web socket request, where body is JSON-ified.
|
||||
*/
|
||||
fun WebSocketRequestMessage.Companion.put(path: String, body: Any): WebSocketRequestMessage {
|
||||
return WebSocketRequestMessage(
|
||||
verb = "PUT",
|
||||
path = path,
|
||||
headers = listOf("content-type:application/json"),
|
||||
body = JsonUtil.toJsonByteString(body),
|
||||
id = SecureRandom().nextLong()
|
||||
)
|
||||
}
|
||||
@@ -253,11 +253,7 @@ public class PushServiceSocket {
|
||||
|
||||
private static final String CALLING_RELAYS = "/v2/calling/relays";
|
||||
|
||||
private static final String PROVISIONING_CODE_PATH = "/v1/devices/provisioning/code";
|
||||
private static final String PROVISIONING_MESSAGE_PATH = "/v1/provisioning/%s";
|
||||
private static final String DEVICE_PATH = "/v1/devices/%s";
|
||||
private static final String WAIT_FOR_DEVICES_PATH = "/v1/devices/wait_for_linked_device/%s?timeout=%s";
|
||||
private static final String TRANSFER_ARCHIVE_PATH = "/v1/devices/transfer_archive";
|
||||
private static final String SET_RESTORE_METHOD_PATH = "/v1/devices/restore_account/%s";
|
||||
private static final String WAIT_RESTORE_METHOD_PATH = "/v1/devices/restore_account/%s?timeout=%s";
|
||||
|
||||
@@ -698,29 +694,6 @@ public class PushServiceSocket {
|
||||
makeServiceRequest(SET_ACCOUNT_ATTRIBUTES, "PUT", JsonUtil.toJson(accountAttributes));
|
||||
}
|
||||
|
||||
public LinkedDeviceVerificationCodeResponse getLinkedDeviceVerificationCode() throws IOException {
|
||||
String responseText = makeServiceRequest(PROVISIONING_CODE_PATH, "GET", null, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
|
||||
return JsonUtil.fromJson(responseText, LinkedDeviceVerificationCodeResponse.class);
|
||||
}
|
||||
|
||||
public List<DeviceInfo> getDevices() throws IOException {
|
||||
String responseText = makeServiceRequest(String.format(DEVICE_PATH, ""), "GET", null);
|
||||
return JsonUtil.fromJson(responseText, DeviceInfoList.class).getDevices();
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a long-polling endpoint that relies on the fact that our normal connection timeout is already 30s.
|
||||
*/
|
||||
public WaitForLinkedDeviceResponse waitForLinkedDevice(String token, int timeoutSeconds) throws IOException {
|
||||
String response = makeServiceRequest(String.format(Locale.US, WAIT_FOR_DEVICES_PATH, token, timeoutSeconds), "GET", null, NO_HEADERS, LONG_POLL_HANDLER, SealedSenderAccess.NONE);
|
||||
return JsonUtil.fromJsonResponse(response, WaitForLinkedDeviceResponse.class);
|
||||
}
|
||||
|
||||
public void setLinkedDeviceTransferArchive(SetLinkedDeviceTransferArchiveRequest request) throws IOException {
|
||||
String body = JsonUtil.toJson(request);
|
||||
makeServiceRequest(String.format(Locale.US, TRANSFER_ARCHIVE_PATH), "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
|
||||
}
|
||||
|
||||
public void setRestoreMethodChosen(@Nonnull String token, @Nonnull RestoreMethodBody request) throws IOException {
|
||||
String body = JsonUtil.toJson(request);
|
||||
makeServiceRequest(String.format(Locale.US, SET_RESTORE_METHOD_PATH, urlEncode(token)), "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
|
||||
@@ -734,10 +707,6 @@ public class PushServiceSocket {
|
||||
return JsonUtil.fromJsonResponse(response, RestoreMethodBody.class);
|
||||
}
|
||||
|
||||
public void removeDevice(long deviceId) throws IOException {
|
||||
makeServiceRequest(String.format(DEVICE_PATH, String.valueOf(deviceId)), "DELETE", null);
|
||||
}
|
||||
|
||||
public void sendProvisioningMessage(String destination, byte[] body) throws IOException {
|
||||
makeServiceRequest(String.format(PROVISIONING_MESSAGE_PATH, urlEncode(destination)), "PUT",
|
||||
JsonUtil.toJson(new ProvisioningMessage(Base64.encodeWithPadding(body))));
|
||||
|
||||
@@ -37,6 +37,7 @@ import java.util.concurrent.TimeoutException
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.signal.libsignal.net.ChatConnection.Request as LibSignalRequest
|
||||
|
||||
@@ -95,17 +96,16 @@ class LibSignalChatConnection(
|
||||
const val SIGNAL_SERVICE_ENVELOPE_TIMESTAMP_HEADER_KEY = "X-Signal-Timestamp"
|
||||
|
||||
private val TAG = Log.tag(LibSignalChatConnection::class.java)
|
||||
private val SEND_TIMEOUT: Long = 10.seconds.inWholeMilliseconds
|
||||
|
||||
private val KEEP_ALIVE_REQUEST = LibSignalRequest(
|
||||
"GET",
|
||||
"/v1/keepalive",
|
||||
emptyMap(),
|
||||
ByteArray(0),
|
||||
SEND_TIMEOUT.toInt()
|
||||
WebSocketConnection.DEFAULT_SEND_TIMEOUT.inWholeMilliseconds.toInt()
|
||||
)
|
||||
|
||||
private fun WebSocketRequestMessage.toLibSignalRequest(timeout: Long = SEND_TIMEOUT): LibSignalRequest {
|
||||
private fun WebSocketRequestMessage.toLibSignalRequest(timeout: Duration = WebSocketConnection.DEFAULT_SEND_TIMEOUT): LibSignalRequest {
|
||||
return LibSignalRequest(
|
||||
this.verb?.uppercase() ?: "GET",
|
||||
this.path ?: "",
|
||||
@@ -117,7 +117,7 @@ class LibSignalChatConnection(
|
||||
parts[0] to parts[1]
|
||||
},
|
||||
this.body?.toByteArray() ?: byteArrayOf(),
|
||||
timeout.toInt()
|
||||
timeout.inWholeMilliseconds.toInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -254,7 +254,7 @@ class LibSignalChatConnection(
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendRequest(request: WebSocketRequestMessage): Single<WebsocketResponse> {
|
||||
override fun sendRequest(request: WebSocketRequestMessage, timeoutSeconds: Long): Single<WebsocketResponse> {
|
||||
CHAT_SERVICE_LOCK.withLock {
|
||||
if (isDead()) {
|
||||
return Single.error(IOException("$name is closed!"))
|
||||
@@ -294,7 +294,7 @@ class LibSignalChatConnection(
|
||||
return single.subscribeOn(Schedulers.io()).observeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
val internalRequest = request.toLibSignalRequest()
|
||||
val internalRequest = request.toLibSignalRequest(timeout = timeoutSeconds.seconds)
|
||||
chatConnection!!.send(internalRequest)
|
||||
.whenComplete(
|
||||
onSuccess = { response ->
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.whispersystems.signalservice.internal.websocket;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.signal.libsignal.protocol.logging.Log;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
@@ -227,7 +228,7 @@ public class OkHttpWebSocketConnection extends WebSocketListener implements WebS
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Single<WebsocketResponse> sendRequest(WebSocketRequestMessage request) throws IOException {
|
||||
public synchronized Single<WebsocketResponse> sendRequest(@NotNull WebSocketRequestMessage request, long timeoutSeconds) throws IOException {
|
||||
if (client == null) {
|
||||
throw new IOException("No connection!");
|
||||
}
|
||||
@@ -247,7 +248,7 @@ public class OkHttpWebSocketConnection extends WebSocketListener implements WebS
|
||||
|
||||
return single.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.timeout(10, TimeUnit.SECONDS, Schedulers.io());
|
||||
.timeout(timeoutSeconds, TimeUnit.SECONDS, Schedulers.io());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.TimeoutException
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Common interface for the web socket connection API
|
||||
@@ -15,6 +16,10 @@ import java.util.concurrent.TimeoutException
|
||||
* - LibSignalChatConnection - the wrapper around libsignal's [org.signal.libsignal.net.ChatService]
|
||||
*/
|
||||
interface WebSocketConnection {
|
||||
companion object {
|
||||
val DEFAULT_SEND_TIMEOUT = 10.seconds
|
||||
}
|
||||
|
||||
val name: String
|
||||
|
||||
fun connect(): Observable<WebSocketConnectionState>
|
||||
@@ -24,7 +29,12 @@ interface WebSocketConnection {
|
||||
fun disconnect()
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun sendRequest(request: WebSocketRequestMessage): Single<WebsocketResponse>
|
||||
fun sendRequest(request: WebSocketRequestMessage): Single<WebsocketResponse> {
|
||||
return sendRequest(request, DEFAULT_SEND_TIMEOUT.inWholeSeconds)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun sendRequest(request: WebSocketRequestMessage, timeoutSeconds: Long): Single<WebsocketResponse>
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun sendKeepAlive()
|
||||
|
||||
Reference in New Issue
Block a user