From d0db1953399ab932be7eae148186fe620f30d4ed Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 6 Aug 2025 16:01:04 -0400 Subject: [PATCH] Add basic auth support for experimental networking features --- mobile/lib/services/api.service.dart | 3 +- mobile/lib/utils/url_helper.dart | 15 +-- mobile/test/services/auth.service_test.dart | 2 +- mobile/test/utils/url_helper_test.dart | 102 ++++++++++++++++++++ 4 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 mobile/test/utils/url_helper_test.dart diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index fca9080c86..d0eb29a98a 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -92,8 +92,9 @@ class ApiService implements Authentication { /// Takes a server URL and attempts to resolve the API endpoint. /// - /// Input: [schema://]host[:port][/path] + /// Input: [schema://][basicAuth:password@]host[:port][/path] /// schema - optional (default: https) + /// userInfo - optional /// host - required /// port - optional (default: based on schema) /// path - optional diff --git a/mobile/lib/utils/url_helper.dart b/mobile/lib/utils/url_helper.dart index e3d5b8ed57..f972d86aa4 100644 --- a/mobile/lib/utils/url_helper.dart +++ b/mobile/lib/utils/url_helper.dart @@ -16,12 +16,15 @@ String? getServerUrl() { if (serverUri == null) { return null; } - - return Uri.decodeFull( - serverUri.hasPort - ? "${serverUri.scheme}://${serverUri.host}:${serverUri.port}" - : "${serverUri.scheme}://${serverUri.host}", - ); + var URIToDecodeFull = "${serverUri.scheme}://"; + if (serverUri.userInfo.isNotEmpty){ + URIToDecodeFull += "${serverUri.userInfo}@"; + } + URIToDecodeFull += "${serverUri.host}"; + if(serverUri.hasPort){ + URIToDecodeFull += ":${serverUri.port}"; + } + return Uri.decodeFull(URIToDecodeFull); } /// Converts a Unicode URL to its ASCII-compatible encoding (Punycode). diff --git a/mobile/test/services/auth.service_test.dart b/mobile/test/services/auth.service_test.dart index 1bad780ca7..0e43505718 100644 --- a/mobile/test/services/auth.service_test.dart +++ b/mobile/test/services/auth.service_test.dart @@ -59,7 +59,7 @@ void main() { db.writeTxnSync(() => db.clearSync()); await StoreService.init(storeRepository: IsarStoreRepository(db)); }); - + // add test here test('Should resolve HTTP endpoint', () async { const testUrl = 'http://ip:2283'; const resolvedUrl = 'http://ip:2283/api'; diff --git a/mobile/test/utils/url_helper_test.dart b/mobile/test/utils/url_helper_test.dart new file mode 100644 index 0000000000..05b325316a --- /dev/null +++ b/mobile/test/utils/url_helper_test.dart @@ -0,0 +1,102 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/utils/url_helper.dart'; +import 'package:isar/isar.dart'; + +import '../test_utils.dart'; +import '../fixtures/user.stub.dart'; + +void main() { + late Isar db; + + setUpAll(() async { + db = await TestUtils.initIsar(); + await StoreService.init(storeRepository: IsarStoreRepository(db)); + }); + + + group('sanitizeUrl', () { + test('Should encode correctly', () { + var unEncodedURL = 'user:password@example.com/addSchemaAndRemovesSlashes////'; + expect(sanitizeUrl(unEncodedURL), 'https://user:password@example.com/addSchemaAndRemovesSlashes'); + }); + + test('does not switch http to https', () { + var unEncodedURL = 'http://user:password@example.com/addSchemaAndRemovesSlashes////'; + expect(sanitizeUrl(unEncodedURL), 'http://user:password@example.com/addSchemaAndRemovesSlashes'); + }); + }); + + group('punycodeEncode', () { + test('malformed URL returns a blank string', () { + var badURL = 'example.com/missing a scheme'; + expect(punycodeEncodeUrl(badURL), ''); + }); + + test('Encodes IDNs correctly', () { + var idn = 'https://bücher.de'; + expect(punycodeEncodeUrl(idn), 'https://xn--bcher-kva.de'); + }); + + test('Keeps basic auth in encoding', () { + var basicAuthURL = 'https://user:password@example.com'; + expect(punycodeEncodeUrl(basicAuthURL), 'https://user:password@example.com'); + }); + }); + + group('punycodeDecode', () { + test('malformed URL returns a null', () { + var badURL = 'example.com/missing%20a%20scheme'; + expect(punycodeDecodeUrl(badURL), null); + }); + + test('Decodes IDNs correctly', () { + var idn = 'https://xn--bcher-kva.de'; + expect(punycodeDecodeUrl(idn), 'https://bücher.de'); + }); + + test('Keeps basic auth in encoding', () { + var basicAuthURL = 'https://user:password@example.com/%20with%20spaces'; + expect(punycodeDecodeUrl(basicAuthURL), 'https://user:password@example.com/ with spaces'); + }); + }); + + group('getServerUrl', () { + test('Returns null if not set', () { + expect(getServerUrl(), null); + }); + + test('Returns null if what was set is not a correct url', () { + Store.put(StoreKey.serverEndpoint, 'example.com'); + expect(getServerUrl(), null); + }); + + test('Returns decoded basic URL', () async { + var encodedURL = 'https://example.com/ clears extra paths'; + await Store.put(StoreKey.serverEndpoint, encodedURL); + expect(getServerUrl(), 'https://example.com'); + }); + + test('Returns decoded basic auth URL', () async { + var basicAuthURL = 'https://user:password@example.com'; + await Store.put(StoreKey.serverEndpoint, basicAuthURL); + expect(getServerUrl(), basicAuthURL); + }); + + test('Returns decoded URL with a port', () async { + var portURL = 'https://example.com:1337'; + await Store.put(StoreKey.serverEndpoint, portURL); + expect(getServerUrl(), portURL); + }); + + test('Returns decoded complex URL', () async { + var complexURL = 'https://user:password@example.com:1337/123/abc'; + await Store.put(StoreKey.serverEndpoint, complexURL); + expect(getServerUrl(), 'https://user:password@example.com:1337'); + }); + + }); +}