diff --git a/Cargo.lock b/Cargo.lock
index afc6219ad..c50ae6fec 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1824,6 +1824,7 @@ dependencies = [
"http 1.1.0",
"jni 0.21.1",
"libsignal-bridge-macros",
+ "libsignal-core",
"libsignal-message-backup",
"libsignal-net",
"libsignal-protocol",
diff --git a/bin/update_versions.py b/bin/update_versions.py
index 9bddb70d1..d52da39db 100755
--- a/bin/update_versions.py
+++ b/bin/update_versions.py
@@ -32,6 +32,7 @@ PODSPEC_PATTERN = re.compile(r"^(.*\.version\s+=\s+')(.*)(')")
GRADLE_PATTERN = re.compile(r'^(\s+version\s+=\s+")(.*)(")')
NODE_PATTERN = re.compile(r'^(\s+"version": ")(.*)(")')
CARGO_PATTERN = re.compile(r'^(version = ")(.*)(")')
+RUST_PATTERN = re.compile(r'^(pub const VERSION: &str = ")(.*)(")')
def bridge_path(bridge):
@@ -48,6 +49,7 @@ def main():
update_version('LibSignalClient.podspec', PODSPEC_PATTERN, new_version)
update_version(os.path.join('java', 'build.gradle'), GRADLE_PATTERN, new_version)
update_version(os.path.join('node', 'package.json'), NODE_PATTERN, new_version)
+ update_version(os.path.join('rust', 'core', 'src', 'version.rs'), RUST_PATTERN, new_version)
update_version(bridge_path('ffi'), CARGO_PATTERN, new_version)
update_version(bridge_path('jni'), CARGO_PATTERN, new_version)
update_version(bridge_path('node'), CARGO_PATTERN, new_version)
diff --git a/java/client/src/main/java/org/signal/libsignal/net/Network.java b/java/client/src/main/java/org/signal/libsignal/net/Network.java
index b1ac49746..c0eb484b7 100644
--- a/java/client/src/main/java/org/signal/libsignal/net/Network.java
+++ b/java/client/src/main/java/org/signal/libsignal/net/Network.java
@@ -35,9 +35,9 @@ public class Network {
*/
private final Svr3 svr3;
- public Network(Environment env) {
+ public Network(Environment env, String userAgent) {
this.tokioAsyncContext = new TokioAsyncContext();
- this.connectionManager = new ConnectionManager(env);
+ this.connectionManager = new ConnectionManager(env, userAgent);
this.svr3 = new Svr3(this);
}
@@ -45,7 +45,7 @@ public class Network {
* Sets the proxy host to be used for all new connections (until overridden).
*
*
Sets a domain name and port to be used to proxy all new outgoing connections. The proxy can
- * be overridden by calling this method again or unset by calling {@link clearProxy}.
+ * be overridden by calling this method again or unset by calling {@link #clearProxy}.
*/
public void setProxy(String host, int port) {
this.connectionManager.setProxy(host, port);
@@ -54,7 +54,7 @@ public class Network {
/**
* Ensures that future connections will be made directly, not through a proxy.
*
- *
Clears any proxy configuration set via {@link setProxy}. If none was set, calling this
+ *
Clears any proxy configuration set via {@link #setProxy}. If none was set, calling this
* method is a no-op.
*/
public void clearProxy() {
@@ -116,8 +116,8 @@ public class Network {
}
static class ConnectionManager extends NativeHandleGuard.SimpleOwner {
- private ConnectionManager(Environment env) {
- super(Native.ConnectionManager_new(env.value));
+ private ConnectionManager(Environment env, String userAgent) {
+ super(Native.ConnectionManager_new(env.value, userAgent));
}
private void setProxy(String host, int port) {
diff --git a/java/client/src/test/java/org/signal/libsignal/net/ChatServiceTest.java b/java/client/src/test/java/org/signal/libsignal/net/ChatServiceTest.java
index 237f6b113..ff166fc70 100644
--- a/java/client/src/test/java/org/signal/libsignal/net/ChatServiceTest.java
+++ b/java/client/src/test/java/org/signal/libsignal/net/ChatServiceTest.java
@@ -16,6 +16,8 @@ import org.signal.libsignal.util.TestEnvironment;
public class ChatServiceTest {
+ private static final String USER_AGENT = "test";
+
private static final int EXPECTED_STATUS = 200;
private static final String EXPECTED_MESSAGE = "OK";
@@ -24,7 +26,7 @@ public class ChatServiceTest {
private static final Map EXPECTED_HEADERS =
Map.of(
- "user-agent", "test",
+ "content-type", "application/octet-stream",
"forwarded", "1.1.1.1");
@Test
@@ -106,7 +108,7 @@ public class ChatServiceTest {
final String PROXY_SERVER = TestEnvironment.get("LIBSIGNAL_TESTING_PROXY_SERVER");
Assume.assumeNotNull(PROXY_SERVER);
- final Network net = new Network(Network.Environment.STAGING);
+ final Network net = new Network(Network.Environment.STAGING, USER_AGENT);
final ChatService chat = net.createChatService("", "");
// Just make sure we can connect.
chat.connectUnauthenticated().get();
@@ -119,7 +121,7 @@ public class ChatServiceTest {
Assume.assumeNotNull(PROXY_SERVER);
// The default TLS proxy config doesn't support staging, so we connect to production.
- final Network net = new Network(Network.Environment.PRODUCTION);
+ final Network net = new Network(Network.Environment.PRODUCTION, USER_AGENT);
final String[] proxyComponents = PROXY_SERVER.split(":");
switch (proxyComponents.length) {
case 1:
diff --git a/java/client/src/test/java/org/signal/libsignal/net/Svr3Test.java b/java/client/src/test/java/org/signal/libsignal/net/Svr3Test.java
index 70949d371..4b63fe9d2 100644
--- a/java/client/src/test/java/org/signal/libsignal/net/Svr3Test.java
+++ b/java/client/src/test/java/org/signal/libsignal/net/Svr3Test.java
@@ -21,6 +21,8 @@ import org.signal.libsignal.util.TestEnvironment;
public class Svr3Test {
+ private static final String USER_AGENT = "test";
+
private final byte[] STORED_SECRET =
Hex.fromStringCondensedAssert(
"d2ae1668ac8a2bfd6170498332babad7cd72b9314631559a361310eee0a8adc6");
@@ -49,7 +51,7 @@ public class Svr3Test {
@Test
public void backupAndRestore() throws Exception {
- Network net = new Network(Network.Environment.STAGING);
+ Network net = new Network(Network.Environment.STAGING, USER_AGENT);
byte[] restored =
net.svr3()
.backup(STORED_SECRET, "password", 2, this.auth)
@@ -60,7 +62,7 @@ public class Svr3Test {
@Test
public void noMoreTries() throws Exception {
- Network net = new Network(Network.Environment.STAGING);
+ Network net = new Network(Network.Environment.STAGING, USER_AGENT);
// Backup and first restore should succeed
byte[] shareSet = net.svr3().backup(STORED_SECRET, "password", 1, this.auth).get();
net.svr3().restore("password", shareSet, this.auth).get();
@@ -75,7 +77,7 @@ public class Svr3Test {
@Test
public void failedRestore() throws Exception {
- Network net = new Network(Network.Environment.STAGING);
+ Network net = new Network(Network.Environment.STAGING, USER_AGENT);
byte[] shareSet = net.svr3().backup(STORED_SECRET, "password", 1, this.auth).get();
try {
net.svr3().restore("wrong password", shareSet, this.auth).get();
@@ -87,7 +89,7 @@ public class Svr3Test {
@Test
public void zeroTries() throws Exception {
- Network net = new Network(Network.Environment.STAGING);
+ Network net = new Network(Network.Environment.STAGING, USER_AGENT);
assertThrows(
IllegalArgumentException.class,
() -> net.svr3().backup(STORED_SECRET, "password", 0, this.auth).get());
@@ -95,7 +97,7 @@ public class Svr3Test {
@Test
public void badSecret() throws Exception {
- Network net = new Network(Network.Environment.STAGING);
+ Network net = new Network(Network.Environment.STAGING, USER_AGENT);
try {
net.svr3().backup(new byte[31], "password", 1, this.auth).get();
} catch (ExecutionException ex) {
@@ -106,7 +108,7 @@ public class Svr3Test {
@Test
public void badShareSet() throws Exception {
- Network net = new Network(Network.Environment.STAGING);
+ Network net = new Network(Network.Environment.STAGING, USER_AGENT);
byte[] shareSet = net.svr3().backup(STORED_SECRET, "password", 1, this.auth).get();
shareSet[0] ^= 0xff;
try {
diff --git a/java/shared/java/org/signal/libsignal/internal/Native.java b/java/shared/java/org/signal/libsignal/internal/Native.java
index 894e586bc..a020688c1 100644
--- a/java/shared/java/org/signal/libsignal/internal/Native.java
+++ b/java/shared/java/org/signal/libsignal/internal/Native.java
@@ -175,7 +175,7 @@ public final class Native {
public static native void ConnectionManager_Destroy(long handle);
public static native void ConnectionManager_clear_proxy(long connectionManager);
- public static native long ConnectionManager_new(int environment);
+ public static native long ConnectionManager_new(int environment, String userAgent);
public static native void ConnectionManager_set_proxy(long connectionManager, String host, int port);
public static native void CreateCallLinkCredentialPresentation_CheckValidContents(byte[] presentationBytes) throws Exception;
diff --git a/node/Native.d.ts b/node/Native.d.ts
index 7dc6e6f94..d0b7ec784 100644
--- a/node/Native.d.ts
+++ b/node/Native.d.ts
@@ -178,7 +178,7 @@ export function CiphertextMessage_FromPlaintextContent(m: Wrapper): Buffer;
export function CiphertextMessage_Type(msg: Wrapper): number;
export function ConnectionManager_clear_proxy(connectionManager: Wrapper): void;
-export function ConnectionManager_new(environment: number): ConnectionManager;
+export function ConnectionManager_new(environment: number, userAgent: string): ConnectionManager;
export function ConnectionManager_set_ipv6_enabled(connectionManager: Wrapper, ipv6Enabled: boolean): void;
export function ConnectionManager_set_proxy(connectionManager: Wrapper, host: string, port: number): void;
export function CreateCallLinkCredentialPresentation_CheckValidContents(presentationBytes: Buffer): void;
diff --git a/node/ts/net.ts b/node/ts/net.ts
index f15407547..9a3487dff 100644
--- a/node/ts/net.ts
+++ b/node/ts/net.ts
@@ -186,9 +186,11 @@ export class Net {
*/
svr3: Svr3Client;
- constructor(env: Environment) {
+ constructor(env: Environment, userAgent: string) {
this.asyncContext = newNativeHandle(Native.TokioAsyncContext_new());
- this.connectionManager = newNativeHandle(Native.ConnectionManager_new(env));
+ this.connectionManager = newNativeHandle(
+ Native.ConnectionManager_new(env, userAgent)
+ );
this.svr3 = new Svr3ClientImpl(this.asyncContext, this.connectionManager);
}
diff --git a/node/ts/test/NetTest.ts b/node/ts/test/NetTest.ts
index 67810c8ec..954d1b2e7 100644
--- a/node/ts/test/NetTest.ts
+++ b/node/ts/test/NetTest.ts
@@ -18,6 +18,8 @@ use(chaiAsPromised);
util.initLogger();
config.truncateThreshold = 0;
+const userAgent = 'test';
+
describe('chat service api', () => {
it('converts ChatServiceError to native', () => {
expect(() => Native.TESTING_ChatServiceErrorConvert())
@@ -34,7 +36,7 @@ describe('chat service api', () => {
it('converts Response object to native', () => {
const status = 200;
const headers: ReadonlyArray<[string, string]> = [
- ['user-agent', 'test'],
+ ['content-type', 'application/octet-stream'],
['forwarded', '1.1.1.1'],
];
const expectedWithContent: ChatResponse = {
@@ -69,11 +71,11 @@ describe('chat service api', () => {
const verb = 'GET';
const path = '/test';
- const userAgent = 'test';
+ const contentType = 'application/octet-stream';
const forwarded = '1.1.1.1';
const content = Buffer.from('content');
const headers: Array<[string, string]> = [
- ['user-agent', userAgent],
+ ['content-type', contentType],
['forwarded', forwarded],
];
@@ -88,8 +90,8 @@ describe('chat service api', () => {
expect(Native.TESTING_ChatRequestGetPath(request)).equals(path);
expect(Native.TESTING_ChatRequestGetBody(request)).deep.equals(content);
expect(
- Native.TESTING_ChatRequestGetHeaderValue(request, 'user-agent')
- ).equals(userAgent);
+ Native.TESTING_ChatRequestGetHeaderValue(request, 'content-type')
+ ).equals(contentType);
expect(
Native.TESTING_ChatRequestGetHeaderValue(request, 'forwarded')
).equals(forwarded);
@@ -129,7 +131,7 @@ describe('chat service api', () => {
});
it('can connect unauthenticated', async () => {
- const net = new Net(Environment.Staging);
+ const net = new Net(Environment.Staging, userAgent);
const chatService = net.newChatService();
await chatService.connectUnauthenticated();
await chatService.disconnect();
@@ -140,7 +142,7 @@ describe('chat service api', () => {
assert(PROXY_SERVER, 'checked above');
// The default TLS proxy config doesn't support staging, so we connect to production.
- const net = new Net(Environment.Production);
+ const net = new Net(Environment.Production, userAgent);
const [host = PROXY_SERVER, port = '443'] = PROXY_SERVER.split(':', 2);
net.setProxy(host, parseInt(port, 10));
@@ -243,7 +245,7 @@ describe('cdsi lookup', () => {
describe('SVR3', () => {
const USERNAME = randomBytes(16).toString('hex');
- const SVR3 = new Net(Environment.Staging).svr3;
+ const SVR3 = new Net(Environment.Staging, userAgent).svr3;
function make_auth(): Readonly {
const otp = Native.CreateOTPFromBase64(
diff --git a/rust/bridge/shared/Cargo.toml b/rust/bridge/shared/Cargo.toml
index 17477b9a6..412c7bd5c 100644
--- a/rust/bridge/shared/Cargo.toml
+++ b/rust/bridge/shared/Cargo.toml
@@ -14,6 +14,7 @@ license = "AGPL-3.0-only"
attest = { path = "../../attest" }
device-transfer = { path = "../../device-transfer" }
libsignal-bridge-macros = { path = "macros" }
+libsignal-core = { path = "../../core" }
libsignal-message-backup = { path = "../../message-backup" }
libsignal-net = { path = "../../net" }
libsignal-protocol = { path = "../../protocol" }
diff --git a/rust/bridge/shared/src/net.rs b/rust/bridge/shared/src/net.rs
index 0cb61382f..70aaaa647 100644
--- a/rust/bridge/shared/src/net.rs
+++ b/rust/bridge/shared/src/net.rs
@@ -24,7 +24,7 @@ use libsignal_net::chat::{
use libsignal_net::enclave::{
Cdsi, EnclaveEndpoint, EnclaveEndpointConnection, EnclaveKind, Nitro, PpssSetup, Sgx, Tpm2Snp,
};
-use libsignal_net::env::{Env, Svr3Env};
+use libsignal_net::env::{add_user_agent_header, Env, Svr3Env};
use libsignal_net::infra::connection_manager::MultiRouteConnectionManager;
use libsignal_net::infra::dns::DnsResolver;
use libsignal_net::infra::tcp_ssl::{
@@ -106,7 +106,7 @@ impl RefUnwindSafe for ConnectionManager {}
impl ConnectionManager {
const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
- fn new(environment: Environment) -> Self {
+ fn new(environment: Environment, user_agent: String) -> Self {
let dns_resolver =
DnsResolver::new_with_static_fallback(environment.env().static_fallback());
let transport_connector =
@@ -116,6 +116,7 @@ impl ConnectionManager {
.env()
.chat_domain_config
.connection_params_with_fallback();
+ let chat_connection_params = add_user_agent_header(chat_connection_params, &user_agent);
let chat_ws_config = make_ws_config(chat_endpoint, Self::DEFAULT_CONNECT_TIMEOUT);
Self {
chat: EndpointConnection::new_multi(
@@ -123,11 +124,11 @@ impl ConnectionManager {
Self::DEFAULT_CONNECT_TIMEOUT,
chat_ws_config,
),
- cdsi: Self::endpoint_connection(environment.env().cdsi),
+ cdsi: Self::endpoint_connection(environment.env().cdsi, &user_agent),
svr3: (
- Self::endpoint_connection(environment.env().svr3.sgx()),
- Self::endpoint_connection(environment.env().svr3.nitro()),
- Self::endpoint_connection(environment.env().svr3.tpm2snp()),
+ Self::endpoint_connection(environment.env().svr3.sgx(), &user_agent),
+ Self::endpoint_connection(environment.env().svr3.nitro(), &user_agent),
+ Self::endpoint_connection(environment.env().svr3.tpm2snp(), &user_agent),
),
transport_connector,
}
@@ -135,8 +136,10 @@ impl ConnectionManager {
fn endpoint_connection(
endpoint: EnclaveEndpoint<'static, E>,
+ user_agent: &str,
) -> EnclaveEndpointConnection {
let params = endpoint.domain_config.connection_params_with_fallback();
+ let params = add_user_agent_header(params, user_agent);
EnclaveEndpointConnection::new_multi(
endpoint.mr_enclave,
params,
@@ -146,8 +149,11 @@ impl ConnectionManager {
}
#[bridge_fn]
-fn ConnectionManager_new(environment: AsType) -> ConnectionManager {
- ConnectionManager::new(environment.into_inner())
+fn ConnectionManager_new(
+ environment: AsType,
+ user_agent: String,
+) -> ConnectionManager {
+ ConnectionManager::new(environment.into_inner(), user_agent)
}
#[bridge_fn]
@@ -559,6 +565,6 @@ mod test {
#[test_case(Environment::Staging; "staging")]
#[test_case(Environment::Prod; "prod")]
fn can_create_connection_manager(env: Environment) {
- let _ = ConnectionManager::new(env);
+ let _ = ConnectionManager::new(env, "test-user-agent".to_string());
}
}
diff --git a/rust/bridge/shared/src/testing/net.rs b/rust/bridge/shared/src/testing/net.rs
index 4aefeaeaa..9bf64bc7b 100644
--- a/rust/bridge/shared/src/testing/net.rs
+++ b/rust/bridge/shared/src/testing/net.rs
@@ -150,7 +150,10 @@ fn TESTING_ChatServiceResponseConvert(
false => None,
};
let mut headers = HeaderMap::new();
- headers.append(http::header::USER_AGENT, HeaderValue::from_static("test"));
+ headers.append(
+ http::header::CONTENT_TYPE,
+ HeaderValue::from_static("application/octet-stream"),
+ );
headers.append(http::header::FORWARDED, HeaderValue::from_static("1.1.1.1"));
Ok(ChatResponse {
status: StatusCode::OK,
diff --git a/rust/core/src/lib.rs b/rust/core/src/lib.rs
index 08a1dcea0..fa8d6726d 100644
--- a/rust/core/src/lib.rs
+++ b/rust/core/src/lib.rs
@@ -4,6 +4,9 @@
//
mod address;
+mod version;
+
pub use address::{
Aci, DeviceId, Pni, ProtocolAddress, ServiceId, ServiceIdFixedWidthBinaryBytes, ServiceIdKind,
};
+pub use version::VERSION;
diff --git a/rust/core/src/version.rs b/rust/core/src/version.rs
new file mode 100644
index 000000000..59c97247f
--- /dev/null
+++ b/rust/core/src/version.rs
@@ -0,0 +1,8 @@
+//
+// Copyright 2024 Signal Messenger, LLC.
+// SPDX-License-Identifier: AGPL-3.0-only
+//
+
+// The value of this constant is updated by the script
+// and should not be manually modified
+pub const VERSION: &str = "0.45.1";
diff --git a/rust/net/src/auth.rs b/rust/net/src/auth.rs
index eda57c68c..bf0894c81 100644
--- a/rust/net/src/auth.rs
+++ b/rust/net/src/auth.rs
@@ -17,7 +17,10 @@ pub trait HttpBasicAuth {
impl From for HttpRequestDecorator {
fn from(value: T) -> Self {
- HttpRequestDecorator::HeaderAuth(basic_authorization(value.username(), value.password()))
+ HttpRequestDecorator::Header(
+ http::header::AUTHORIZATION,
+ basic_authorization(value.username(), value.password()),
+ )
}
}
diff --git a/rust/net/src/chat.rs b/rust/net/src/chat.rs
index 7bb0deaff..c7fcf413a 100644
--- a/rust/net/src/chat.rs
+++ b/rust/net/src/chat.rs
@@ -427,8 +427,10 @@ fn build_authorized_chat_service(
username: String,
password: String,
) -> AuthorizedChatService {
- let header_auth_decorator =
- HttpRequestDecorator::HeaderAuth(basic_authorization(&username, &password));
+ let header_auth_decorator = HttpRequestDecorator::Header(
+ http::header::AUTHORIZATION,
+ basic_authorization(&username, &password),
+ );
// ws authorized
let chat_over_ws_auth = ServiceWithReconnect::new(
diff --git a/rust/net/src/env.rs b/rust/net/src/env.rs
index 65270939c..c86a140f6 100644
--- a/rust/net/src/env.rs
+++ b/rust/net/src/env.rs
@@ -192,6 +192,20 @@ impl DomainConfig {
}
}
+pub fn add_user_agent_header(
+ mut connection_params_list: Vec,
+ user_agent: &str,
+) -> Vec {
+ let with_lib_version = format!("{} libsignal/{}", user_agent, libsignal_core::VERSION);
+ connection_params_list.iter_mut().for_each(|cp| {
+ cp.http_request_decorator.add(HttpRequestDecorator::Header(
+ http::header::USER_AGENT,
+ http::header::HeaderValue::try_from(&with_lib_version).expect("valid header string"),
+ ));
+ });
+ connection_params_list
+}
+
pub struct ProxyConfig {
route_type: RouteType,
hostname: &'static str,
diff --git a/rust/net/src/infra.rs b/rust/net/src/infra.rs
index b5c5815c2..77facde6d 100644
--- a/rust/net/src/infra.rs
+++ b/rust/net/src/infra.rs
@@ -52,11 +52,8 @@ impl IpType {
/// A collection of commonly used decorators for HTTP requests.
#[derive(Clone, Debug)]
pub enum HttpRequestDecorator {
- /// Adds the following header to the request:
- /// ```text
- /// Authorization: Basic base64(:)
- /// ```
- HeaderAuth(String),
+ /// Adds a specific header to the request
+ Header(http::header::HeaderName, http::header::HeaderValue),
/// Prefixes the path portion of the request with the given string.
PathPrefix(&'static str),
/// Applies generic decoration logic.
@@ -72,6 +69,12 @@ impl From for HttpRequestDecoratorSeq {
}
}
+impl HttpRequestDecoratorSeq {
+ pub fn add(&mut self, decorator: HttpRequestDecorator) {
+ self.0.push(decorator)
+ }
+}
+
/// Contains all information required to establish an HTTP connection to the remote endpoint:
/// - `sni` value to be used in TLS,
/// - `host` value to be used for DNS resolution an in the HTTP requests headers,
@@ -194,7 +197,7 @@ impl HttpRequestDecorator {
fn decorate_request(&self, request_builder: http::request::Builder) -> http::request::Builder {
match self {
Self::Generic(decorator) => decorator(request_builder),
- Self::HeaderAuth(auth) => request_builder.header(::http::header::AUTHORIZATION, auth),
+ Self::Header(name, value) => request_builder.header(name, value),
Self::PathPrefix(prefix) => {
let uri = request_builder.uri_ref().expect("request has URI set");
let mut parts = (*uri).clone().into_parts();
@@ -454,8 +457,11 @@ pub(crate) mod test {
fn test_header_auth_decorator() {
let expected = "Basic dXNybm06cHNzd2Q=";
let builder = Request::get("https://chat.signal.org/");
- let builder = HttpRequestDecorator::HeaderAuth(basic_authorization("usrnm", "psswd"))
- .decorate_request(builder);
+ let builder = HttpRequestDecorator::Header(
+ http::header::AUTHORIZATION,
+ basic_authorization("usrnm", "psswd"),
+ )
+ .decorate_request(builder);
let (parts, _) = builder.body(()).unwrap().into_parts();
assert_eq!(
expected,
diff --git a/rust/net/src/utils.rs b/rust/net/src/utils.rs
index 11da5b8f3..dc604e25b 100644
--- a/rust/net/src/utils.rs
+++ b/rust/net/src/utils.rs
@@ -6,15 +6,16 @@
use base64::prelude::{Engine as _, BASE64_STANDARD};
use futures_util::stream::FuturesUnordered;
use futures_util::StreamExt;
+use http::HeaderValue;
use std::future;
use std::future::Future;
use std::time::Duration;
/// Constructs the value of the `Authorization` header for the `Basic` auth scheme.
-pub(crate) fn basic_authorization(username: &str, password: &str) -> String {
+pub(crate) fn basic_authorization(username: &str, password: &str) -> HeaderValue {
let auth = BASE64_STANDARD.encode(format!("{}:{}", username, password).as_bytes());
let auth = format!("Basic {}", auth);
- auth
+ HeaderValue::try_from(auth).expect("valid header value")
}
/// Requires a `Future` to complete before the specified duration has elapsed.
diff --git a/swift/Sources/LibSignalClient/Net.swift b/swift/Sources/LibSignalClient/Net.swift
index b1b130072..2a5dc8b3a 100644
--- a/swift/Sources/LibSignalClient/Net.swift
+++ b/swift/Sources/LibSignalClient/Net.swift
@@ -26,9 +26,9 @@ public class Net {
public let svr3: Svr3Client
/// Creates a new `Net` instance that enables interacting with services in the given Signal environment.
- public init(env: Environment) {
+ public init(env: Environment, userAgent: String) {
self.asyncContext = TokioAsyncContext()
- self.connectionManager = ConnectionManager(env: env)
+ self.connectionManager = ConnectionManager(env: env, userAgent: userAgent)
self.svr3 = Svr3Client(self.asyncContext, self.connectionManager)
}
@@ -373,9 +373,9 @@ internal class TokioAsyncContext: NativeHandleOwner {
}
internal class ConnectionManager: NativeHandleOwner {
- convenience init(env: Net.Environment) {
+ convenience init(env: Net.Environment, userAgent: String) {
var handle: OpaquePointer?
- failOnError(signal_connection_manager_new(&handle, env.rawValue))
+ failOnError(signal_connection_manager_new(&handle, env.rawValue, userAgent))
self.init(owned: handle!)
}
diff --git a/swift/Sources/SignalFfi/signal_ffi.h b/swift/Sources/SignalFfi/signal_ffi.h
index e2e3e7b47..1ac8130bc 100644
--- a/swift/Sources/SignalFfi/signal_ffi.h
+++ b/swift/Sources/SignalFfi/signal_ffi.h
@@ -1397,7 +1397,7 @@ SignalFfiError *signal_tokio_async_context_new(SignalTokioAsyncContext **out);
SignalFfiError *signal_tokio_async_context_destroy(SignalTokioAsyncContext *p);
-SignalFfiError *signal_connection_manager_new(SignalConnectionManager **out, uint8_t environment);
+SignalFfiError *signal_connection_manager_new(SignalConnectionManager **out, uint8_t environment, const char *user_agent);
SignalFfiError *signal_connection_manager_set_proxy(const SignalConnectionManager *connection_manager, const char *host, uint16_t port);
diff --git a/swift/Tests/LibSignalClientTests/ChatServiceTests.swift b/swift/Tests/LibSignalClientTests/ChatServiceTests.swift
index 4e4a7442a..649b92e85 100644
--- a/swift/Tests/LibSignalClientTests/ChatServiceTests.swift
+++ b/swift/Tests/LibSignalClientTests/ChatServiceTests.swift
@@ -12,10 +12,11 @@ import SignalFfi
import XCTest
final class ChatServiceTests: TestCaseBase {
+ private static let userAgent = "test"
private static let expectedStatus: UInt16 = 200
private static let expectedMessage = "OK"
private static let expectedContent = "content".data(using: .utf8)
- private static let expectedHeaders = ["user-agent": "test", "forwarded": "1.1.1.1"]
+ private static let expectedHeaders = ["content-type": "application/octet-stream", "forwarded": "1.1.1.1"]
func testConvertResponse() throws {
do {
@@ -111,7 +112,7 @@ final class ChatServiceTests: TestCaseBase {
throw XCTSkip()
}
- let net = Net(env: .staging)
+ let net = Net(env: .staging, userAgent: Self.userAgent)
let chat = net.createChatService(username: "", password: "")
// Just make sure we can connect.
try await chat.connectUnauthenticated()
@@ -124,7 +125,7 @@ final class ChatServiceTests: TestCaseBase {
}
// The default TLS proxy config doesn't support staging, so we connect to production.
- let net = Net(env: .production)
+ let net = Net(env: .production, userAgent: Self.userAgent)
let host: Substring
let port: UInt16
if let colonIndex = PROXY_SERVER.firstIndex(of: ":") {
diff --git a/swift/Tests/LibSignalClientTests/NetTests.swift b/swift/Tests/LibSignalClientTests/NetTests.swift
index 621ba05ac..5280fd89e 100644
--- a/swift/Tests/LibSignalClientTests/NetTests.swift
+++ b/swift/Tests/LibSignalClientTests/NetTests.swift
@@ -11,6 +11,8 @@ import Foundation
import SignalFfi
import XCTest
+let userAgent: String = "test"
+
final class NetTests: XCTestCase {
func testCdsiLookupResultConversion() async throws {
let ACI_UUID = "9d0652a3-dcc3-4d11-975f-74d61598733f"
@@ -114,7 +116,7 @@ final class NetTests: XCTestCase {
token: nil,
returnAcisWithoutUaks: false
)
- let net = Net(env: .staging)
+ let net = Net(env: .staging, userAgent: userAgent)
let lookup = try await net.cdsiLookup(auth: auth, request: request)
let response = try await lookup.complete()
@@ -139,7 +141,7 @@ final class Svr3Tests: TestCaseBase {
func testBackupAndRestore() async throws {
let auth = try Auth(username: self.username, enclaveSecret: self.getEnclaveSecret())
- let net = Net(env: .staging)
+ let net = Net(env: .staging, userAgent: userAgent)
let shareSet = try await net.svr3.backup(
self.storedSecret,
@@ -158,7 +160,7 @@ final class Svr3Tests: TestCaseBase {
func testInvalidPassword() async throws {
let auth = try Auth(username: self.username, enclaveSecret: self.getEnclaveSecret())
- let net = Net(env: .staging)
+ let net = Net(env: .staging, userAgent: userAgent)
let shareSet = try await net.svr3.backup(
self.storedSecret,
@@ -183,7 +185,7 @@ final class Svr3Tests: TestCaseBase {
func testCorruptedShareSet() async throws {
let auth = try Auth(username: self.username, enclaveSecret: self.getEnclaveSecret())
- let net = Net(env: .staging)
+ let net = Net(env: .staging, userAgent: userAgent)
var shareSet = try await net.svr3.backup(
self.storedSecret,
@@ -210,7 +212,7 @@ final class Svr3Tests: TestCaseBase {
func testMaxRetries() async throws {
let auth = try Auth(username: self.username, enclaveSecret: self.getEnclaveSecret())
- let net = Net(env: .staging)
+ let net = Net(env: .staging, userAgent: userAgent)
let shareSet = try await net.svr3.backup(
self.storedSecret,
@@ -241,7 +243,7 @@ final class Svr3Tests: TestCaseBase {
func testMaxRetriesAfterFailure() async throws {
let auth = try Auth(username: self.username, enclaveSecret: self.getEnclaveSecret())
- let net = Net(env: .staging)
+ let net = Net(env: .staging, userAgent: userAgent)
let shareSet = try await net.svr3.backup(
self.storedSecret,
@@ -279,7 +281,7 @@ final class Svr3Tests: TestCaseBase {
func testInvalidMaxTries() async throws {
let auth = try Auth(username: self.username, enclaveSecret: self.getEnclaveSecret())
- let net = Net(env: .staging)
+ let net = Net(env: .staging, userAgent: userAgent)
do {
_ = try await net.svr3.backup(
@@ -298,7 +300,7 @@ final class Svr3Tests: TestCaseBase {
func testInvalidSecretSize() async throws {
let auth = try Auth(username: self.username, enclaveSecret: self.getEnclaveSecret())
- let net = Net(env: .staging)
+ let net = Net(env: .staging, userAgent: userAgent)
do {
_ = try await net.svr3.backup(