From 1e101851393f1300b1786c782b33c0e0c977a193 Mon Sep 17 00:00:00 2001 From: Eric Bariaux <375613+ebariaux@users.noreply.github.com> Date: Thu, 10 Apr 2025 12:06:01 +0200 Subject: [PATCH 1/5] Fix parsing of domain to URL, add tests --- ORLib.xcodeproj/project.pbxproj | 8 +++ ORLib/ConfigManager.swift | 4 +- ORLib/Utils/String+Utils.swift | 46 +++++++++++++++ Tests/StringUtilsTest.swift | 97 ++++++++++++++++++++++++++++++++ Tests/URLTest.swift | 99 +++++++++++++++++++++++++++++++++ 5 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 Tests/StringUtilsTest.swift create mode 100644 Tests/URLTest.swift diff --git a/ORLib.xcodeproj/project.pbxproj b/ORLib.xcodeproj/project.pbxproj index 0e954fe..8df3647 100644 --- a/ORLib.xcodeproj/project.pbxproj +++ b/ORLib.xcodeproj/project.pbxproj @@ -43,6 +43,8 @@ 4CFEC51C2C919AF300DCC936 /* ORLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CBDF2AA2AE285E400C7D94C /* ORLib.framework */; }; 4CFEC51D2C919AF300DCC936 /* ORLib.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4CBDF2AA2AE285E400C7D94C /* ORLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 914F7D4429215D3500655A22 /* test12.json in Resources */ = {isa = PBXBuildFile; fileRef = 914F7D4329215D3500655A22 /* test12.json */; }; + 9154E2A02D9EB0D50055E565 /* StringUtilsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9154E29F2D9EB0D50055E565 /* StringUtilsTest.swift */; }; + 9154E2A22D9EB3220055E565 /* URLTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9154E2A12D9EB3220055E565 /* URLTest.swift */; }; 9156512A28FC6D6700062E16 /* test9.json in Resources */ = {isa = PBXBuildFile; fileRef = 9156512928FC6D6700062E16 /* test9.json */; }; 91932F9328C66A3C00BABBA3 /* test1.json in Resources */ = {isa = PBXBuildFile; fileRef = 91932F9228C66A3C00BABBA3 /* test1.json */; }; 91932F9628C6715C00BABBA3 /* test2.json in Resources */ = {isa = PBXBuildFile; fileRef = 91932F9528C6715C00BABBA3 /* test2.json */; }; @@ -116,6 +118,8 @@ 4CF5D06727143F1F00D705BE /* ORNotificationResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ORNotificationResource.swift; sourceTree = ""; }; 4CF5D06827143F1F00D705BE /* HttpApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpApiManager.swift; sourceTree = ""; }; 914F7D4329215D3500655A22 /* test12.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = test12.json; sourceTree = ""; }; + 9154E29F2D9EB0D50055E565 /* StringUtilsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtilsTest.swift; sourceTree = ""; }; + 9154E2A12D9EB3220055E565 /* URLTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTest.swift; sourceTree = ""; }; 9156512928FC6D6700062E16 /* test9.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = test9.json; sourceTree = ""; }; 91658F9028897E55000FF05C /* ORConsoleConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ORConsoleConfig.swift; sourceTree = ""; }; 91658F9228897EDD000FF05C /* ORAppInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ORAppInfo.swift; sourceTree = ""; }; @@ -296,6 +300,8 @@ 91A9A8FA28BF6A4900DF8928 /* ConfigManagerTest.swift */, 91A9A90028BF6EA000DF8928 /* FileApiManager.swift */, 91AA79F228D628E9005B9913 /* Fixture.swift */, + 9154E29F2D9EB0D50055E565 /* StringUtilsTest.swift */, + 9154E2A12D9EB3220055E565 /* URLTest.swift */, ); path = Tests; sourceTree = ""; @@ -463,6 +469,8 @@ buildActionMask = 2147483647; files = ( 91A9A90128BF6EA000DF8928 /* FileApiManager.swift in Sources */, + 9154E2A22D9EB3220055E565 /* URLTest.swift in Sources */, + 9154E2A02D9EB0D50055E565 /* StringUtilsTest.swift in Sources */, 91A9A8FB28BF6A4900DF8928 /* ConfigManagerTest.swift in Sources */, 91AA79F328D628E9005B9913 /* Fixture.swift in Sources */, ); diff --git a/ORLib/ConfigManager.swift b/ORLib/ConfigManager.swift index 7626296..9bbfc13 100644 --- a/ORLib/ConfigManager.swift +++ b/ORLib/ConfigManager.swift @@ -40,7 +40,7 @@ public class ConfigManager { public func setDomain(domain: String) async throws -> ConfigManagerState { switch state { case .selectDomain: - let baseUrl = domain.isValidURL ? domain : "https://\(domain).openremote.app" + let baseUrl = domain.buildBaseUrlFromDomain() let url = baseUrl.appending("/api/master") apiManager = apiManagerFactory(url) @@ -124,7 +124,7 @@ public class ConfigManager { } } - + private func filterPotentialApps(apiManager: ApiManager, potentialApps: [String]?) async -> [String]? { var filteredApps : [String]? if let appNames = potentialApps { diff --git a/ORLib/Utils/String+Utils.swift b/ORLib/Utils/String+Utils.swift index b9ea1bf..1a0c7bd 100644 --- a/ORLib/Utils/String+Utils.swift +++ b/ORLib/Utils/String+Utils.swift @@ -31,4 +31,50 @@ extension String { } } + func buildBaseUrlFromDomain() -> String { + do { + let pattern = "^(?:([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6}))$" + let ipv6NoSchemeNoPort = try NSRegularExpression(pattern: pattern) + let numberOfMatches = ipv6NoSchemeNoPort.numberOfMatches(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count)) + if numberOfMatches == 1 { + return "https://[\(self)]" + } + } catch let error as NSError { + print("Error creating NSRegularExpression: \(error)") + } + /* + do { + let ipv4 = try NSRegularExpression(pattern: "^http(s)?://((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.)){3}+((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$") + let numberOfMatches = ipv4.numberOfMatches(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count)) + if numberOfMatches == 1 { + return self + } + } catch let error as NSError { + print("Error creating NSRegularExpression: \(error)") + } + + do { + let ipv4 = try NSRegularExpression(pattern: "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.)){3}+((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$") + let numberOfMatches = ipv4.numberOfMatches(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count)) + if numberOfMatches == 1 { + return "https://\(self)" + } + } catch let error as NSError { + print("Error creating NSRegularExpression: \(error)") + } + */ + + if self.starts(with: "https://") || self.starts(with: "http://") { + if self.firstIndex(of: ".") != nil || self.firstIndex(of: "[") != nil { + return self + } else { + return "\(self).openremote.app" + } + } else if self.firstIndex(of: ".") != nil || self.firstIndex(of: "[") != nil { + return "https://\(self)" + } + return "https://\(self).openremote.app" + } + + } diff --git a/Tests/StringUtilsTest.swift b/Tests/StringUtilsTest.swift new file mode 100644 index 0000000..9483fc3 --- /dev/null +++ b/Tests/StringUtilsTest.swift @@ -0,0 +1,97 @@ +/* + * Copyright 2017, OpenRemote Inc. + * + * See the CONTRIBUTORS.txt file in the distribution for a + * full listing of individual contributors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import Testing +@testable import ORLib + +struct StringUtilsTest { + + @Test func fqdnWithScheme() async throws { + #expect("http://www.example.com".buildBaseUrlFromDomain() == "http://www.example.com") + #expect("https://www.example.com".buildBaseUrlFromDomain() == "https://www.example.com") + } + + @Test func fqdnNoScheme() async throws { + #expect("www.example.com".buildBaseUrlFromDomain() == "https://www.example.com") + } + + @Test func fqdnAndPortWithScheme() async throws { + #expect("http://www.example.com:8080".buildBaseUrlFromDomain() == "http://www.example.com:8080") + #expect("https://www.example.com:443".buildBaseUrlFromDomain() == "https://www.example.com:443") + } + + @Test func fqdnAndPortNoScheme() async throws { + #expect("www.example.com:8080".buildBaseUrlFromDomain() == "https://www.example.com:8080") + } + + @Test func hostnameNoScheme() async throws { + #expect("example".buildBaseUrlFromDomain() == "https://example.openremote.app") + } + + @Test func ipAddressWithScheme () async throws { + #expect("http://192.168.1.1".buildBaseUrlFromDomain() == "http://192.168.1.1") + } + + @Test func ipAddressAndPortWithScheme () async throws { + #expect("http://192.168.1.1:8080".buildBaseUrlFromDomain() == "http://192.168.1.1:8080") + } + + @Test func ipAddressNoScheme () async throws { + #expect("192.168.1.1".buildBaseUrlFromDomain() == "https://192.168.1.1") + } + + @Test func ipAddressAndPortNoScheme () async throws { + #expect("192.168.1.1:8080".buildBaseUrlFromDomain() == "https://192.168.1.1:8080") + } + + @Test func ipv6AddressWithScheme () async throws { + #expect("http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]".buildBaseUrlFromDomain() == "http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]") + } + + @Test func ipv6AddressAndPortWithScheme () async throws { + #expect("http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080".buildBaseUrlFromDomain() == "http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080") + } + + @Test func ipv6AddressNoScheme () async throws { + #expect("2001:0db8:85a3:0000:0000:8a2e:0370:7334".buildBaseUrlFromDomain() == "https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]") + #expect("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]".buildBaseUrlFromDomain() == "https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]") + } + + @Test func ipv6AddressAndPortNoScheme () async throws { + #expect("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080".buildBaseUrlFromDomain() == "https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080") + } + + @Test func ipv6CompressedAddressWithScheme () async throws { + #expect("http://[2001:db8:85a3::8a2e:370:7334]".buildBaseUrlFromDomain() == "http://[2001:db8:85a3::8a2e:370:7334]") + } + + @Test func ipv6CompressedAddressAndPortWithScheme () async throws { + #expect("http://[2001:db8:85a3::8a2e:370:7334]:8080".buildBaseUrlFromDomain() == "http://[2001:db8:85a3::8a2e:370:7334]:8080") + } + + @Test func ipv6CompressedAddressNoScheme () async throws { + #expect("2001:db8:85a3::8a2e:370:7334".buildBaseUrlFromDomain() == "https://[2001:db8:85a3::8a2e:370:7334]") + #expect("[2001:db8:85a3::8a2e:370:7334]".buildBaseUrlFromDomain() == "https://[2001:db8:85a3::8a2e:370:7334]") + } + + @Test func ipv6CompressedAddressAndPortNoScheme () async throws { + #expect("[2001:db8:85a3::8a2e:370:7334]:8080".buildBaseUrlFromDomain() == "https://[2001:db8:85a3::8a2e:370:7334]:8080") + } +} diff --git a/Tests/URLTest.swift b/Tests/URLTest.swift new file mode 100644 index 0000000..51d4a00 --- /dev/null +++ b/Tests/URLTest.swift @@ -0,0 +1,99 @@ +/* + * Copyright 2017, OpenRemote Inc. + * + * See the CONTRIBUTORS.txt file in the distribution for a + * full listing of individual contributors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import Testing +@testable import ORLib + +/// Tests to understand how URL parsing works +struct URLTest { + + @Test func FQDNWithScheme() async throws { + let url = URL(string: "https://www.example.com") + #expect(url != nil) + #expect(url!.host == "www.example.com") + #expect(url!.scheme == "https") + #expect(url!.port == nil) + } + + @Test func FQDNWithCustomScheme() async throws { + let url = URL(string: "myscheme://www.example.com") + #expect(url != nil) + #expect(url!.host == "www.example.com") + #expect(url!.scheme == "myscheme") + #expect(url!.port == nil) + } + + @Test func FQDNWithSchemeAndPort() async throws { + let url = URL(string: "https://www.example.com:1234") + #expect(url != nil) + #expect(url!.host == "www.example.com") + #expect(url!.scheme == "https") + #expect(url!.port == 1234) + } + + @Test func FQDNNoScheme() async throws { + let url = URL(string: "www.example.com") + #expect(url != nil) + #expect(url!.host == nil) + #expect(url!.scheme == nil) + #expect(url!.port == nil) + } + + @Test func hostnameWithScheme() async throws { + let url = URL(string: "http://example") + #expect(url != nil) + #expect(url!.host == "example") + #expect(url!.scheme == "http") + #expect(url!.port == nil) + } + + @Test func hostnameNoScheme() async throws { + let url = URL(string: "example") + #expect(url != nil) + #expect(url!.host == nil) + #expect(url!.scheme == nil) + #expect(url!.port == nil) + } + + @Test func ipWithScheme() async throws { + let url = URL(string: "http://192.168.1.1") + #expect(url != nil) + #expect(url!.host == "192.168.1.1") + #expect(url!.scheme == "http") + #expect(url!.port == nil) + } + + @Test func ipNoScheme() async throws { + let url = URL(string: "192.168.1.1") + #expect(url != nil) + #expect(url!.host == nil) + #expect(url!.scheme == nil) + #expect(url!.port == nil) + } + + /// ! URL does not validate it's an IP address + @Test func invalidIpNoScheme() async throws { + let url = URL(string: "432.168.1.1") + #expect(url != nil) + #expect(url!.host == nil) + #expect(url!.scheme == nil) + #expect(url!.port == nil) + } +} From aa6ac18b0280828efda303d73601eaf41e635b5f Mon Sep 17 00:00:00 2001 From: Eric Bariaux <375613+ebariaux@users.noreply.github.com> Date: Thu, 10 Apr 2025 12:11:18 +0200 Subject: [PATCH 2/5] Clean-up unused code --- ORLib/Utils/String+Utils.swift | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/ORLib/Utils/String+Utils.swift b/ORLib/Utils/String+Utils.swift index 1a0c7bd..f1b1f41 100644 --- a/ORLib/Utils/String+Utils.swift +++ b/ORLib/Utils/String+Utils.swift @@ -20,16 +20,6 @@ extension String { return encodedString } - - var isValidURL: Bool { - let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) - if let match = detector.firstMatch(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count)) { - // it is a link, if the match covers the whole string - return match.range.length == self.utf16.count - } else { - return false - } - } func buildBaseUrlFromDomain() -> String { do { @@ -42,27 +32,6 @@ extension String { } catch let error as NSError { print("Error creating NSRegularExpression: \(error)") } - /* - do { - let ipv4 = try NSRegularExpression(pattern: "^http(s)?://((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.)){3}+((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$") - let numberOfMatches = ipv4.numberOfMatches(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count)) - if numberOfMatches == 1 { - return self - } - } catch let error as NSError { - print("Error creating NSRegularExpression: \(error)") - } - - do { - let ipv4 = try NSRegularExpression(pattern: "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.)){3}+((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$") - let numberOfMatches = ipv4.numberOfMatches(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count)) - if numberOfMatches == 1 { - return "https://\(self)" - } - } catch let error as NSError { - print("Error creating NSRegularExpression: \(error)") - } - */ if self.starts(with: "https://") || self.starts(with: "http://") { if self.firstIndex(of: ".") != nil || self.firstIndex(of: "[") != nil { @@ -76,5 +45,4 @@ extension String { return "https://\(self).openremote.app" } - } From 9de3889d960d300f536ead82f8da3f7576bf3262 Mon Sep 17 00:00:00 2001 From: Eric Bariaux <375613+ebariaux@users.noreply.github.com> Date: Thu, 10 Apr 2025 12:15:08 +0200 Subject: [PATCH 3/5] Reformat and license --- ORLib/Utils/String+Utils.swift | 41 +++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/ORLib/Utils/String+Utils.swift b/ORLib/Utils/String+Utils.swift index f1b1f41..6ea0576 100644 --- a/ORLib/Utils/String+Utils.swift +++ b/ORLib/Utils/String+Utils.swift @@ -1,26 +1,35 @@ -// -// String+Utils.swift -// GenericApp -// -// Created by Michael Rademaker on 26/10/2020. -// Copyright © 2020 OpenRemote. All rights reserved. -// +/* + * Copyright 2017, OpenRemote Inc. + * + * See the CONTRIBUTORS.txt file in the distribution for a + * full listing of individual contributors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ import Foundation extension String { - func stringByURLEncoding() -> String? { - - let characters = CharacterSet.urlQueryAllowed.union(CharacterSet(charactersIn: "#")) - - guard let encodedString = self.addingPercentEncoding(withAllowedCharacters: characters) else { - return nil + func stringByURLEncoding() -> String? { + let characters = CharacterSet.urlQueryAllowed.union(CharacterSet(charactersIn: "#")) + guard let encodedString = self.addingPercentEncoding(withAllowedCharacters: characters) else { + return nil + } + return encodedString } - return encodedString - } - func buildBaseUrlFromDomain() -> String { do { let pattern = "^(?:([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6}))$" From 1988479e1c312469988e4fd6743d5c1eaeb27601 Mon Sep 17 00:00:00 2001 From: Eric Bariaux <375613+ebariaux@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:04:05 +0200 Subject: [PATCH 4/5] Handle non web schemes in URL --- ORLib/Utils/String+Utils.swift | 12 +++++++++++- Tests/StringUtilsTest.swift | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/ORLib/Utils/String+Utils.swift b/ORLib/Utils/String+Utils.swift index 6ea0576..35941b5 100644 --- a/ORLib/Utils/String+Utils.swift +++ b/ORLib/Utils/String+Utils.swift @@ -30,6 +30,8 @@ extension String { return encodedString } + /// There is no validation that the generated string represents a valid URL. + /// For instance, no validation is performed on the port if one is provided. func buildBaseUrlFromDomain() -> String { do { let pattern = "^(?:([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6}))$" @@ -42,7 +44,15 @@ extension String { print("Error creating NSRegularExpression: \(error)") } - if self.starts(with: "https://") || self.starts(with: "http://") { + let numberOfMatches: Int + do { + let schemePrefix = try NSRegularExpression(pattern: "^[a-zA-Z]+://.*$") + numberOfMatches = schemePrefix.numberOfMatches(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count)) + } catch let error as NSError { + numberOfMatches = 0 + print("Error creating NSRegularExpression: \(error)") + } + if numberOfMatches == 1 { if self.firstIndex(of: ".") != nil || self.firstIndex(of: "[") != nil { return self } else { diff --git a/Tests/StringUtilsTest.swift b/Tests/StringUtilsTest.swift index 9483fc3..c4af13b 100644 --- a/Tests/StringUtilsTest.swift +++ b/Tests/StringUtilsTest.swift @@ -28,6 +28,10 @@ struct StringUtilsTest { #expect("https://www.example.com".buildBaseUrlFromDomain() == "https://www.example.com") } + @Test func fqdnWithNonWebScheme() async throws { + #expect("ftp://www.example.com".buildBaseUrlFromDomain() == "ftp://www.example.com") + } + @Test func fqdnNoScheme() async throws { #expect("www.example.com".buildBaseUrlFromDomain() == "https://www.example.com") } @@ -37,6 +41,10 @@ struct StringUtilsTest { #expect("https://www.example.com:443".buildBaseUrlFromDomain() == "https://www.example.com:443") } + @Test func fqdnAndPortWithNonWebScheme() async throws { + #expect("ftp://www.example.com:21".buildBaseUrlFromDomain() == "ftp://www.example.com:21") + } + @Test func fqdnAndPortNoScheme() async throws { #expect("www.example.com:8080".buildBaseUrlFromDomain() == "https://www.example.com:8080") } @@ -49,10 +57,22 @@ struct StringUtilsTest { #expect("http://192.168.1.1".buildBaseUrlFromDomain() == "http://192.168.1.1") } + @Test func ipAddressWithNonWebScheme () async throws { + #expect("ftp://192.168.1.1".buildBaseUrlFromDomain() == "ftp://192.168.1.1") + } + @Test func ipAddressAndPortWithScheme () async throws { #expect("http://192.168.1.1:8080".buildBaseUrlFromDomain() == "http://192.168.1.1:8080") } + @Test func ipAddressAndPortWithNonWebScheme () async throws { + #expect("ftp://192.168.1.1:25".buildBaseUrlFromDomain() == "ftp://192.168.1.1:25") + } + + @Test func ipAddressAndInvalidPortWithScheme () async throws { + #expect("http://192.168.1.1:InvalidPort".buildBaseUrlFromDomain() == "http://192.168.1.1:InvalidPort") + } + @Test func ipAddressNoScheme () async throws { #expect("192.168.1.1".buildBaseUrlFromDomain() == "https://192.168.1.1") } @@ -94,4 +114,5 @@ struct StringUtilsTest { @Test func ipv6CompressedAddressAndPortNoScheme () async throws { #expect("[2001:db8:85a3::8a2e:370:7334]:8080".buildBaseUrlFromDomain() == "https://[2001:db8:85a3::8a2e:370:7334]:8080") } + } From 4d35ae39fcaf175f19faa763a4dac7e3db33c7c3 Mon Sep 17 00:00:00 2001 From: Eric Bariaux <375613+ebariaux@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:05:14 +0200 Subject: [PATCH 5/5] URL built from domain can still be invalid, don't force unwrap but throw an error --- ORLib/ConfigManager.swift | 8 ++++---- ORLib/Network/ApiManager.swift | 1 + ORLib/Network/HttpApiManager.swift | 7 +++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/ORLib/ConfigManager.swift b/ORLib/ConfigManager.swift index 9bbfc13..c79af4c 100644 --- a/ORLib/ConfigManager.swift +++ b/ORLib/ConfigManager.swift @@ -20,12 +20,12 @@ public enum ConfigManagerError: Error { case couldNotLoadAppConfig } -public typealias ApiManagerFactory = (String) -> ApiManager +public typealias ApiManagerFactory = (String) throws -> ApiManager public class ConfigManager { - private var apiManagerFactory: ((String) -> ApiManager) + private var apiManagerFactory: ApiManagerFactory private var apiManager: ApiManager? public private(set) var globalAppInfos : [String:ORAppInfo] = [:] // app infos from the top level consoleConfig information @@ -43,8 +43,8 @@ public class ConfigManager { let baseUrl = domain.buildBaseUrlFromDomain() let url = baseUrl.appending("/api/master") - apiManager = apiManagerFactory(url) - + apiManager = try apiManagerFactory(url) + guard let api = apiManager else { throw ConfigManagerError.communicationError } diff --git a/ORLib/Network/ApiManager.swift b/ORLib/Network/ApiManager.swift index 09bca83..c1e9703 100644 --- a/ORLib/Network/ApiManager.swift +++ b/ORLib/Network/ApiManager.swift @@ -10,6 +10,7 @@ import Foundation public typealias ResponseBlock = (_ statusCode: Int, _ object: T?, _ error: Error?) -> () public enum ApiManagerError: Error { + case invalidUrl case notFound case communicationError(Int) case parsingError(Int) diff --git a/ORLib/Network/HttpApiManager.swift b/ORLib/Network/HttpApiManager.swift index 3defbbb..159c2d9 100644 --- a/ORLib/Network/HttpApiManager.swift +++ b/ORLib/Network/HttpApiManager.swift @@ -20,8 +20,11 @@ public class HttpApiManager: NSObject, ApiManager { private let baseUrl: URL; - public init(baseUrl: String) { - self.baseUrl = URL(string: baseUrl)! + public init(baseUrl: String) throws { + guard let url = URL(string: baseUrl) else { + throw ApiManagerError.invalidUrl + } + self.baseUrl = url super.init() }