Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions ORLib.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -116,6 +118,8 @@
4CF5D06727143F1F00D705BE /* ORNotificationResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ORNotificationResource.swift; sourceTree = "<group>"; };
4CF5D06827143F1F00D705BE /* HttpApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpApiManager.swift; sourceTree = "<group>"; };
914F7D4329215D3500655A22 /* test12.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = test12.json; sourceTree = "<group>"; };
9154E29F2D9EB0D50055E565 /* StringUtilsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtilsTest.swift; sourceTree = "<group>"; };
9154E2A12D9EB3220055E565 /* URLTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTest.swift; sourceTree = "<group>"; };
9156512928FC6D6700062E16 /* test9.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = test9.json; sourceTree = "<group>"; };
91658F9028897E55000FF05C /* ORConsoleConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ORConsoleConfig.swift; sourceTree = "<group>"; };
91658F9228897EDD000FF05C /* ORAppInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ORAppInfo.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -296,6 +300,8 @@
91A9A8FA28BF6A4900DF8928 /* ConfigManagerTest.swift */,
91A9A90028BF6EA000DF8928 /* FileApiManager.swift */,
91AA79F228D628E9005B9913 /* Fixture.swift */,
9154E29F2D9EB0D50055E565 /* StringUtilsTest.swift */,
9154E2A12D9EB3220055E565 /* URLTest.swift */,
);
path = Tests;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
);
Expand Down
12 changes: 6 additions & 6 deletions ORLib/ConfigManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,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
Expand All @@ -52,11 +52,11 @@ 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)
apiManager = try apiManagerFactory(url)

guard let api = apiManager else {
throw ConfigManagerError.communicationError
}
Expand Down Expand Up @@ -136,7 +136,7 @@ public class ConfigManager {
}

}

private func filterPotentialApps(apiManager: ApiManager, potentialApps: [String]?) async -> [String]? {
var filteredApps : [String]?
if let appNames = potentialApps {
Expand Down
1 change: 1 addition & 0 deletions ORLib/Network/ApiManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Foundation
public typealias ResponseBlock<T: Codable> = (_ statusCode: Int, _ object: T?, _ error: Error?) -> ()

public enum ApiManagerError: Error {
case invalidUrl
case notFound
case communicationError(Int)
case parsingError(Int)
Expand Down
7 changes: 5 additions & 2 deletions ORLib/Network/HttpApiManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,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()
}

Expand Down
53 changes: 37 additions & 16 deletions ORLib/Utils/String+Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,46 @@ 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
}

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
/// 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}))$"
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)")
}

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 {
return "\(self).openremote.app"
}
} else if self.firstIndex(of: ".") != nil || self.firstIndex(of: "[") != nil {
return "https://\(self)"
}
return "https://\(self).openremote.app"
}

}
118 changes: 118 additions & 0 deletions Tests/StringUtilsTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/

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 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")
}

@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 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")
}

@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 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")
}

@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")
}

}
99 changes: 99 additions & 0 deletions Tests/URLTest.swift
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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)
}
}