Move a bunch of files into the network modules.

This commit is contained in:
Greyson Parrelli
2026-05-14 13:23:16 -04:00
committed by Michelle Tang
parent 6339b38dee
commit 4dd57460de
280 changed files with 622 additions and 421 deletions
@@ -0,0 +1,469 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.network
import io.reactivex.rxjava3.core.Single
import org.signal.core.util.concurrent.safeBlockingGet
import org.signal.network.NetworkResult.ApplicationError
import org.signal.network.NetworkResult.StatusCodeError
import org.signal.network.exceptions.MalformedRequestException
import org.signal.network.exceptions.NonSuccessfulResponseCodeException
import org.signal.network.exceptions.PushNetworkException
import org.signal.network.util.JsonUtil
import org.signal.network.websocket.WebsocketResponse
import java.io.IOException
import java.util.concurrent.TimeoutException
import kotlin.reflect.KClass
import kotlin.reflect.cast
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
typealias StatusCodeErrorAction = (StatusCodeError<*>) -> Unit
typealias ApplicationErrorAction = (ApplicationError<*>) -> Unit
/**
* A helper class that wraps the result of a network request, turning common exceptions
* into sealed classes, with optional request chaining.
*
* This was designed to be a middle ground between the heavy reliance on specific exceptions
* in old network code (which doesn't translate well to kotlin not having checked exceptions)
* and plain rx, which still doesn't free you from having to catch exceptions and translate
* things to sealed classes yourself.
*
* If you have a very complicated network request with lots of different possible response types
* based on specific errors, this isn't for you. You're likely better off writing your own
* sealed class. However, for the majority of requests which just require getting a model from
* the success case and the status code of the error, this can be quite convenient.
*/
sealed class NetworkResult<T>(
private val statusCodeErrorActions: MutableSet<StatusCodeErrorAction> = mutableSetOf(),
private val applicationErrorActions: MutableSet<ApplicationErrorAction> = mutableSetOf()
) {
companion object {
/**
* A convenience method to capture the common case of making a request.
* Perform the network action in the [fetcher], returning your result.
* Common exceptions will be caught and translated to errors.
*/
@JvmStatic
fun <T> fromFetch(fetcher: Fetcher<T>): NetworkResult<T> = try {
Success(fetcher.fetch())
} catch (e: NonSuccessfulResponseCodeException) {
StatusCodeError(e)
} catch (e: IOException) {
NetworkError(e)
} catch (e: Throwable) {
ApplicationError(e)
}
/**
* A convenience method to convert a websocket request into a network result, parsing the body into type [T].
*
* Common HTTP errors will be translated to [StatusCodeError]s.
*/
inline fun <reified T : Any> fromWebSocket(fetcher: Fetcher<Single<WebsocketResponse>>): NetworkResult<T> {
return fromWebSocket(DefaultWebSocketConverter(T::class), fetcher)
}
/**
* A convenience method to convert a websocket request into a network result, using the provided
* [webSocketResponseConverter] to parse the response into type [T].
*
* Common HTTP errors will be translated to [StatusCodeError]s.
*/
fun <T> fromWebSocket(
webSocketResponseConverter: WebSocketResponseConverter<T>,
fetcher: Fetcher<Single<WebsocketResponse>>
): NetworkResult<T> {
return try {
val result: Result<NetworkResult<T>> = fetcher.fetch()
.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: InterruptedException) {
NetworkError(PushNetworkException(e))
} catch (e: Throwable) {
ApplicationError(e)
}
}
/**
* Coroutine-friendly variant of the [fromWebSocket] overload that takes a [WebSocketResponseConverter].
*/
suspend fun <T> fromWebSocketSuspend(
webSocketResponseConverter: WebSocketResponseConverter<T>,
fetcher: suspend () -> WebsocketResponse
): NetworkResult<T> {
return try {
webSocketResponseConverter.convert(fetcher())
} catch (e: kotlinx.coroutines.CancellationException) {
throw e
} catch (e: NonSuccessfulResponseCodeException) {
StatusCodeError(e)
} catch (e: IOException) {
NetworkError(e)
} catch (e: TimeoutException) {
NetworkError(PushNetworkException(e))
} catch (e: InterruptedException) {
NetworkError(PushNetworkException(e))
} catch (e: Throwable) {
ApplicationError(e)
}
}
/**
* Wraps a local operation, [block], that may throw an exception that should be wrapped in an [ApplicationError]
* and abort downstream network requests that directly depend on the output of the local operation. Should
* be used almost exclusively prior to a [then].
*/
fun <T : Any> fromLocal(block: () -> T): NetworkResult<T> {
return try {
Success(block())
} catch (e: Throwable) {
ApplicationError(e)
}
}
/**
* Runs [operation] to perform a network call. If [shouldRetry] returns false for the result, then returns it. Otherwise will call [operation] repeatedly
* until [shouldRetry] returns false or is called [maxAttempts] number of times.
*
* @param maxAttempts Max attempts to try the network operation, must be 1 or more, default is 5
* @param shouldRetry Predicate to determine if network operation should be retried, default is any [NetworkError] result is retried
* @param logAttempt Log each attempt before [operation] is called, default is noop
* @param operation Network operation that can be called repeatedly for each attempt
*/
fun <T : Any?> withRetry(
maxAttempts: Int = 5,
shouldRetry: (NetworkResult<T>) -> Boolean = { it is NetworkError },
logAttempt: (attempt: Int, maxAttempts: Int) -> Unit = { _, _ -> },
operation: () -> NetworkResult<T>
): NetworkResult<T> {
require(maxAttempts > 0)
lateinit var result: NetworkResult<T>
for (attempt in 0 until maxAttempts) {
logAttempt(attempt, maxAttempts)
result = operation()
if (!shouldRetry(result)) {
return result
}
}
return result
}
}
/** Indicates the request was successful */
data class Success<T>(val result: T) : NetworkResult<T>()
/** Indicates a generic network error occurred before we were able to process a response. */
data class NetworkError<T>(val exception: IOException) : NetworkResult<T>()
/** Indicates we got a response, but it was a non-2xx response. */
data class StatusCodeError<T>(val code: Int, val stringBody: String?, val binaryBody: ByteArray?, private val headers: Map<String, String>, val exception: NonSuccessfulResponseCodeException) : NetworkResult<T>() {
constructor(e: NonSuccessfulResponseCodeException) : this(e.code, e.stringBody, e.binaryBody, e.headers, e)
constructor(result: StatusCodeError<*>) : this(result.code, result.stringBody, result.binaryBody, result.headers, result.exception)
inline fun <reified T> parseJsonBody(): T? {
return try {
if (stringBody != null) {
JsonUtil.fromJsonResponse(stringBody, T::class.java)
} else if (binaryBody != null) {
JsonUtil.fromJsonResponse(binaryBody, T::class.java)
} else {
null
}
} catch (_: MalformedRequestException) {
null
}
}
fun header(key: String): String? {
return headers[key.lowercase()]
}
fun retryAfter(): Duration? {
return header("retry-after")?.toLongOrNull()?.seconds
}
}
/** Indicates that the application somehow failed in a way unrelated to network activity. Usually a runtime crash. */
data class ApplicationError<T>(val throwable: Throwable) : NetworkResult<T>()
/**
* Returns the result if successful, otherwise turns the result back into an exception and throws it.
*
* Useful for bridging to Java, where you may want to use try-catch.
*/
@Throws(NonSuccessfulResponseCodeException::class, IOException::class, Throwable::class)
fun successOrThrow(): T {
when (this) {
is Success -> return result
is NetworkError -> throw exception
is StatusCodeError -> throw exception
is ApplicationError -> throw throwable
}
}
/**
* Returns the result if successful, otherwise null.
*/
fun successOrNull(): T? {
return when (this) {
is Success -> result
else -> null
}
}
/**
* Returns the [Throwable] associated with the result, or null if the result is successful.
*/
fun getCause(): Throwable? {
return when (this) {
is Success -> null
is NetworkError -> exception
is StatusCodeError -> exception
is ApplicationError -> throwable
}
}
/**
* Takes the output of one [NetworkResult] and transforms it into another if the operation is successful.
* If it's non-successful, [transform] lambda is not run, and instead the original failure will be propagated.
* Useful for changing the type of a result.
*
* If an exception is thrown during [transform], this is mapped to an [ApplicationError].
*
* ```kotlin
* val user: NetworkResult<LocalUserModel> = NetworkResult
* .fromFetch { fetchRemoteUserModel() }
* .map { it.toLocalUserModel() }
* ```
*/
fun <R> map(transform: (T) -> R): NetworkResult<R> {
val map = when (this) {
is Success -> {
try {
Success(transform(this.result))
} catch (e: Throwable) {
ApplicationError<R>(e)
}
}
is NetworkError -> NetworkError<R>(exception)
is ApplicationError -> ApplicationError<R>(throwable)
is StatusCodeError -> StatusCodeError<R>(this)
}
return map.runOnStatusCodeError(statusCodeErrorActions).runOnApplicationError(applicationErrorActions)
}
/**
* Provides the ability to fallback to [fallback] if the current [NetworkResult] is non-successful.
*
* The [fallback] will only be triggered on non-[Success] results. You can provide a [predicate] to limit what kinds of errors you fallback on
* (the default is to fallback on every error).
*
* This primary usecase of this is to make an unauth websocket request and fallback to auth websocket upon failure.
*
* ```kotlin
* val user: NetworkResult<LocalUserModel> = NetworkResult
* .fromWebSocket { unauthWebSocket.request(request, sealedSenderAccess) }
* .fallback { NetworkResult.fromWebSocket { authWebSocket.request(request) } }
* ```
*
* @param predicate If this lambda returns true, the fallback will be triggered.
*/
fun fallback(predicate: (NetworkResult<T>) -> Boolean = { true }, fallback: () -> NetworkResult<T>): NetworkResult<T> {
if (this is Success) {
return this
}
return if (predicate(this)) {
fallback()
} else {
this
}
}
/**
* See [fallback].
*/
suspend fun fallbackSuspend(predicate: (NetworkResult<T>) -> Boolean = { true }, fallback: suspend () -> NetworkResult<T>): NetworkResult<T> {
if (this is Success) {
return this
}
return if (predicate(this)) {
fallback()
} else {
this
}
}
/**
* Takes the output of one [NetworkResult] and passes it as the input to another if the operation is successful.
* If it's non-successful, the [result] lambda is not run, and instead the original failure will be propagated.
* Useful for chaining operations together.
*
* ```kotlin
* val networkResult: NetworkResult<MyData> = NetworkResult
* .fromFetch { fetchAuthCredential() }
* .then {
* NetworkResult.fromFetch { credential -> fetchData(credential) }
* }
* ```
*/
fun <R> then(result: (T) -> NetworkResult<R>): NetworkResult<R> {
val then = when (this) {
is Success -> result(this.result)
is NetworkError -> NetworkError<R>(exception)
is ApplicationError -> ApplicationError<R>(throwable)
is StatusCodeError -> StatusCodeError<R>(this)
}
return then.runOnStatusCodeError(statusCodeErrorActions).runOnApplicationError(applicationErrorActions)
}
/**
* Will perform an operation if the result at this point in the chain is successful. Note that it runs if the chain is _currently_ successful. It does not
* depend on anything further down the chain.
*
* ```kotlin
* val networkResult: NetworkResult<MyData> = NetworkResult
* .fromFetch { fetchAuthCredential() }
* .runIfSuccessful { storeMyCredential(it) }
* ```
*/
fun runIfSuccessful(result: (T) -> Unit): NetworkResult<T> {
if (this is Success) {
result(this.result)
}
return this
}
/**
* Specify an action to be run when a status code error occurs. When a result is a [StatusCodeError] or is transformed into one further down the chain via
* a future [map] or [then], this code will be run. There can only ever be a single status code error in a chain, and therefore this lambda will only ever
* be run a single time.
*
* This is a low-visibility way of doing things, so use sparingly.
*
* ```kotlin
* val result = NetworkResult
* .fromFetch { getAuth() }
* .runOnStatusCodeError { error -> logError(error) }
* .then { credential ->
* NetworkResult.fromFetch { fetchUserDetails(credential) }
* }
* ```
*/
fun runOnStatusCodeError(action: StatusCodeErrorAction): NetworkResult<T> {
return runOnStatusCodeError(setOf(action))
}
private fun runOnStatusCodeError(actions: Collection<StatusCodeErrorAction>): NetworkResult<T> {
if (actions.isEmpty()) {
return this
}
statusCodeErrorActions += actions
if (this is StatusCodeError) {
statusCodeErrorActions.forEach { it.invoke(this) }
statusCodeErrorActions.clear()
}
return this
}
/**
* Specify an action to be run when a application error occurs. When a result is a [ApplicationErrorAction] or is transformed into one further down the chain via
* a future [map] or [then], this code will be run. There can only ever be a single application error in a chain, and therefore this lambda will only ever
* be run a single time.
*
* This is a low-visibility way of doing things, so use sparingly.
*
* ```kotlin
* val result = NetworkResult
* .fromFetch { getAuth() }
* .runOnApplicationError { error -> logError(error) }
* .then { credential ->
* NetworkResult.fromFetch { fetchUserDetails(credential) }
* }
* ```
*/
fun runOnApplicationError(action: ApplicationErrorAction): NetworkResult<T> {
return runOnApplicationError(setOf(action))
}
private fun runOnApplicationError(actions: Collection<ApplicationErrorAction>): NetworkResult<T> {
if (actions.isEmpty()) {
return this
}
applicationErrorActions += actions
if (this is ApplicationError) {
applicationErrorActions.forEach { it.invoke(this) }
applicationErrorActions.clear()
}
return this
}
fun interface Fetcher<T> {
@Throws(Exception::class)
fun fetch(): T
}
fun interface WebSocketResponseConverter<T> {
@Throws(Exception::class)
fun convert(response: WebsocketResponse): NetworkResult<T>
fun <T : Any> WebsocketResponse.toStatusCodeError(): NetworkResult<T> {
return StatusCodeError(NonSuccessfulResponseCodeException(this.status, "", this.body, this.headers))
}
fun <T : Any> WebsocketResponse.toSuccess(responseJsonClass: KClass<T>): NetworkResult<T> {
return when (responseJsonClass) {
Unit::class -> Success(responseJsonClass.cast(Unit))
String::class -> Success(responseJsonClass.cast(this.body))
else -> Success(JsonUtil.fromJson(this.body, responseJsonClass.java))
}
}
}
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)
}
}
}
}
@@ -0,0 +1,8 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.network.exceptions
class MalformedRequestException : NonSuccessfulResponseCodeException(400)
@@ -0,0 +1,17 @@
package org.signal.network.exceptions;
import java.io.IOException;
/**
* Indicates that a response is malformed or otherwise in an unexpected format.
*/
public class MalformedResponseException extends IOException {
public MalformedResponseException(String message) {
super(message);
}
public MalformedResponseException(String message, IOException e) {
super(message, e);
}
}
@@ -0,0 +1,57 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.signal.network.exceptions
import java.io.IOException
/**
* Indicates a server response that is not successful, typically something outside the 2xx range.
*/
open class NonSuccessfulResponseCodeException : IOException {
@JvmField
val code: Int
val stringBody: String?
val binaryBody: ByteArray?
val headers: Map<String, String>
constructor(code: Int) : super("StatusCode: $code") {
this.code = code
this.stringBody = null
this.binaryBody = null
this.headers = emptyMap()
}
constructor(code: Int, message: String) : super("[$code] $message") {
this.code = code
this.stringBody = null
this.binaryBody = null
this.headers = emptyMap()
}
@JvmOverloads
constructor(code: Int, message: String, body: String?, headers: Map<String, String> = emptyMap()) : super("[$code] $message") {
this.code = code
this.stringBody = body
this.binaryBody = null
this.headers = headers.mapKeys { it.key.lowercase() }
}
@JvmOverloads
constructor(code: Int, message: String, body: ByteArray?, headers: Map<String, String> = emptyMap()) : super("[$code] $message") {
this.code = code
this.stringBody = null
this.binaryBody = body
this.headers = headers.mapKeys { it.key.lowercase() }
}
fun is4xx(): Boolean {
return code >= 400 && code < 500
}
fun is5xx(): Boolean {
return code >= 500 && code < 600
}
}
@@ -0,0 +1,21 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.signal.network.exceptions;
import java.io.IOException;
public class PushNetworkException extends IOException {
public PushNetworkException(Exception exception) {
super(exception);
}
public PushNetworkException(String s) {
super(s);
}
}
@@ -0,0 +1,193 @@
/*
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.signal.network.util;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.module.kotlin.KotlinModule;
import org.signal.core.util.Base64;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.logging.Log;
import org.signal.core.models.MasterKey;
import org.signal.core.models.ServiceId;
import org.signal.core.models.ServiceId.ACI;
import org.signal.network.exceptions.MalformedResponseException;
import org.signal.core.util.UuidUtil;
import java.io.IOException;
import java.util.UUID;
import javax.annotation.Nonnull;
import okio.ByteString;
@SuppressWarnings("unused")
public class JsonUtil {
private static final String TAG = JsonUtil.class.getSimpleName();
private static final ObjectMapper objectMapper = new ObjectMapper();
static {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.registerModule(new KotlinModule());
}
public static String toJson(Object object) {
try {
return objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
Log.w(TAG, e);
return "";
}
}
public static @Nonnull ByteString toJsonByteString(@Nonnull Object object) {
return ByteString.of(toJson(object).getBytes());
}
public static <T> T fromJson(String json, Class<T> clazz)
throws IOException
{
return objectMapper.readValue(json, clazz);
}
public static <T> T fromJson(String json, TypeReference<T> typeRef)
throws IOException
{
return objectMapper.readValue(json, typeRef);
}
public static <T> T fromJson(byte[] json, Class<T> clazz)
throws IOException
{
return objectMapper.readValue(json, clazz);
}
public static <T> T fromJsonResponse(String json, TypeReference<T> typeRef)
throws MalformedResponseException
{
try {
return JsonUtil.fromJson(json, typeRef);
} catch (IOException e) {
throw new MalformedResponseException("Unable to parse entity", e);
}
}
public static <T> T fromJsonResponse(String body, Class<T> clazz)
throws MalformedResponseException
{
try {
return JsonUtil.fromJson(body, clazz);
} catch (IOException e) {
throw new MalformedResponseException("Unable to parse entity", e);
}
}
public static <T> T fromJsonResponse(byte[] body, Class<T> clazz)
throws MalformedResponseException
{
try {
return JsonUtil.fromJson(body, clazz);
} catch (IOException e) {
throw new MalformedResponseException("Unable to parse entity", e);
}
}
public static class IdentityKeySerializer extends JsonSerializer<IdentityKey> {
@Override
public void serialize(IdentityKey value, JsonGenerator gen, SerializerProvider serializers)
throws IOException
{
gen.writeString(Base64.encodeWithoutPadding(value.serialize()));
}
}
public static class IdentityKeyDeserializer extends JsonDeserializer<IdentityKey> {
@Override
public IdentityKey deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
try {
return new IdentityKey(Base64.decode(p.getValueAsString()), 0);
} catch (InvalidKeyException e) {
throw new IOException(e);
}
}
}
public static class UuidSerializer extends JsonSerializer<UUID> {
@Override
public void serialize(UUID value, JsonGenerator gen, SerializerProvider serializers)
throws IOException
{
gen.writeString(value.toString());
}
}
public static class UuidDeserializer extends JsonDeserializer<UUID> {
@Override
public UUID deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return UuidUtil.parseOrNull(p.getValueAsString());
}
}
public static class AciSerializer extends JsonSerializer<ACI> {
@Override
public void serialize(ACI value, JsonGenerator gen, SerializerProvider serializers)
throws IOException
{
gen.writeString(value.toString());
}
}
public static class AciDeserializer extends JsonDeserializer<ACI> {
@Override
public ACI deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return ACI.parseOrNull(p.getValueAsString());
}
}
public static class ServiceIdSerializer extends JsonSerializer<ServiceId> {
@Override
public void serialize(ServiceId value, JsonGenerator gen, SerializerProvider serializers)
throws IOException
{
gen.writeString(value.toString());
}
}
public static class ServiceIdDeserializer extends JsonDeserializer<ServiceId> {
@Override
public ServiceId deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return ServiceId.parseOrNull(p.getValueAsString());
}
}
public static class MasterKeySerializer extends JsonSerializer<MasterKey> {
@Override
public void serialize(MasterKey value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(Base64.encodeWithPadding(value.serialize()));
}
}
public static class MasterKeyDeserializer extends JsonDeserializer<MasterKey> {
@Override
public MasterKey deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return new MasterKey(Base64.decode(p.getValueAsString()));
}
}
}
@@ -0,0 +1,41 @@
package org.signal.network.util;
/**
* Convenient ways to assert expected state.
*/
public final class Preconditions {
private Preconditions() {}
public static void checkArgument(boolean state) {
checkArgument(state, "Condition must be true!");
}
public static void checkArgument(boolean state, String message) {
if (!state) {
throw new IllegalArgumentException(message);
}
}
public static void checkState(boolean state) {
checkState(state, "Condition must be true!");
}
public static void checkState(boolean state, String message) {
if (!state) {
throw new IllegalStateException(message);
}
}
public static <E> E checkNotNull(E object) {
return checkNotNull(object, "Must not be null!");
}
public static <E> E checkNotNull(E object, String message) {
if (object == null) {
throw new NullPointerException(message);
} else {
return object;
}
}
}
@@ -0,0 +1,81 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.network.websocket
import okio.ByteString.Companion.toByteString
import org.signal.network.util.JsonUtil
import org.signal.network.websocket.WebSocketRequestMessage
import java.security.SecureRandom
/**
* Create a basic GET web socket request
*/
fun WebSocketRequestMessage.Companion.get(path: String, headers: Map<String, String> = emptyMap()): WebSocketRequestMessage {
return WebSocketRequestMessage(
verb = "GET",
path = path,
headers = headers.toHeaderList(),
id = SecureRandom().nextLong()
)
}
/**
* Create a basic POST web socket request
*/
fun WebSocketRequestMessage.Companion.post(path: String, body: Any?, headers: Map<String, String> = emptyMap()): WebSocketRequestMessage {
return WebSocketRequestMessage(
verb = "POST",
path = path,
body = body?.let { JsonUtil.toJsonByteString(body) },
headers = (if (body != null) listOf("content-type:application/json") else emptyList()) + headers.toHeaderList(),
id = SecureRandom().nextLong()
)
}
/**
* Create a basic DELETE web socket request
*/
fun WebSocketRequestMessage.Companion.delete(path: String, headers: Map<String, String> = emptyMap()): WebSocketRequestMessage {
return WebSocketRequestMessage(
verb = "DELETE",
path = path,
headers = headers.toHeaderList(),
id = SecureRandom().nextLong()
)
}
/**
* Create a basic PUT web socket request, where body is JSON-ified.
*/
fun WebSocketRequestMessage.Companion.put(path: String, body: Any, headers: Map<String, String> = emptyMap()): WebSocketRequestMessage {
return WebSocketRequestMessage(
verb = "PUT",
path = path,
headers = listOf("content-type:application/json") + headers.toHeaderList(),
body = when (body) {
is String -> body.toByteArray().toByteString()
else -> JsonUtil.toJsonByteString(body)
},
id = SecureRandom().nextLong()
)
}
/**
* Create a custom PUT web socket request, where body and content type header are provided by caller.
*/
fun WebSocketRequestMessage.Companion.putCustom(path: String, body: ByteArray, headers: Map<String, String>): WebSocketRequestMessage {
return WebSocketRequestMessage(
verb = "PUT",
path = path,
headers = headers.toHeaderList(),
body = body.toByteString(),
id = SecureRandom().nextLong()
)
}
private fun Map<String, String>.toHeaderList(): List<String> {
return map { (key, value) -> "$key:$value" }
}
@@ -0,0 +1,80 @@
package org.signal.network.websocket;
import org.signal.network.util.Preconditions;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class WebsocketResponse {
private final int status;
private final String body;
private final Map<String, String> headers;
private final boolean unidentified;
public WebsocketResponse(int status, String body, List<String> headers, boolean unidentified) {
this(status, body, parseHeaders(headers), unidentified);
}
public WebsocketResponse(int status, String body, Map<String, String> headerMap, boolean unidentified) {
this.status = status;
this.body = body;
this.headers = headerMap;
this.unidentified = unidentified;
}
public int getStatus() {
return status;
}
public String getBody() {
return body;
}
public String getHeader(String key) {
return headers.get(Preconditions.checkNotNull(key.toLowerCase()));
}
public Map<String, String> getHeaders() {
return headers;
}
public boolean isUnidentified() {
return unidentified;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final WebsocketResponse that = (WebsocketResponse) o;
return status == that.status && unidentified == that.unidentified && Objects.equals(body, that.body) && Objects.equals(headers, that.headers);
}
@Override
public int hashCode() {
return Objects.hash(status, body, headers, unidentified);
}
private static Map<String, String> parseHeaders(List<String> rawHeaders) {
Map<String, String> headers = new HashMap<>(rawHeaders.size());
for (String raw : rawHeaders) {
if (raw != null && !raw.isEmpty()) {
int colonIndex = raw.indexOf(":");
if (colonIndex > 0 && colonIndex < raw.length() - 1) {
String key = raw.substring(0, colonIndex).trim().toLowerCase();
String value = raw.substring(colonIndex + 1).trim();
headers.put(key, value);
}
}
}
return headers;
}
}
@@ -0,0 +1,39 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
syntax = "proto2";
package signalservice;
option java_package = "org.signal.network.websocket";
option java_outer_classname = "WebSocketProtos";
message WebSocketRequestMessage {
optional string verb = 1;
optional string path = 2;
optional bytes body = 3;
repeated string headers = 5;
optional uint64 id = 4;
}
message WebSocketResponseMessage {
optional uint64 id = 1;
optional uint32 status = 2;
optional string message = 3;
repeated string headers = 5;
optional bytes body = 4;
}
message WebSocketMessage {
enum Type {
UNKNOWN = 0;
REQUEST = 1;
RESPONSE = 2;
}
optional Type type = 1;
optional WebSocketRequestMessage request = 2;
optional WebSocketResponseMessage response = 3;
}