From 7ff8f36022ac3d7d6369294589cc6de185c25399 Mon Sep 17 00:00:00 2001 From: Arlo Gilbert Date: Thu, 21 Feb 2019 16:20:33 -0600 Subject: [PATCH] Hello World --- .github/ISSUE_TEMPLATE/bug_report.md | 38 + .github/ISSUE_TEMPLATE/feature_request.md | 20 + .gitignore | 68 + Cartfile | 3 + Cartfile.resolved | 7 + Contrib/logo.png | Bin 0 -> 5172 bytes LICENSE | 21 + .../project.pbxproj | 1269 +++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../PrivacyMonitorFramework.xcscheme | 99 ++ .../PrivacyMonitorFrameworkMac.xcscheme | 99 ++ README.md | 43 + Sources/Configs/Configs.swift | 26 + Sources/Helpers/DatabaseManager.swift | 178 +++ Sources/Info.plist | 24 + Sources/Models/Domain.swift | 42 + Sources/Networking/NetworkManager.swift | 88 ++ Sources/Networking/ServiceError.swift | 27 + .../Networking/Targets/AnalysisService.swift | 44 + Sources/Networking/Targets/ScoreService.swift | 55 + Sources/PrivacyMonitor.swift | 185 +++ Sources/PrivacyMonitorFramework.h | 11 + Sources/Utils/Extensions/Date+Days.swift | 15 + .../Utils/Extensions/String+RootDomain.swift | 34 + Sources/Utils/Extensions/URL+RootDomain.swift | 17 + Tests/DatabaseTests.swift | 78 + Tests/Info.plist | 22 + Tests/NetworkingTests.swift | 133 ++ Tests/RootDomainTests.swift | 49 + Tests/WorkflowTests.swift | 185 +++ 31 files changed, 2895 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .gitignore create mode 100644 Cartfile create mode 100644 Cartfile.resolved create mode 100644 Contrib/logo.png create mode 100644 LICENSE create mode 100644 PrivacyMonitorFramework.xcodeproj/project.pbxproj create mode 100644 PrivacyMonitorFramework.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 PrivacyMonitorFramework.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 PrivacyMonitorFramework.xcodeproj/xcshareddata/xcschemes/PrivacyMonitorFramework.xcscheme create mode 100644 PrivacyMonitorFramework.xcodeproj/xcshareddata/xcschemes/PrivacyMonitorFrameworkMac.xcscheme create mode 100644 README.md create mode 100644 Sources/Configs/Configs.swift create mode 100644 Sources/Helpers/DatabaseManager.swift create mode 100644 Sources/Info.plist create mode 100644 Sources/Models/Domain.swift create mode 100644 Sources/Networking/NetworkManager.swift create mode 100644 Sources/Networking/ServiceError.swift create mode 100644 Sources/Networking/Targets/AnalysisService.swift create mode 100644 Sources/Networking/Targets/ScoreService.swift create mode 100644 Sources/PrivacyMonitor.swift create mode 100644 Sources/PrivacyMonitorFramework.h create mode 100644 Sources/Utils/Extensions/Date+Days.swift create mode 100644 Sources/Utils/Extensions/String+RootDomain.swift create mode 100644 Sources/Utils/Extensions/URL+RootDomain.swift create mode 100644 Tests/DatabaseTests.swift create mode 100644 Tests/Info.plist create mode 100644 Tests/NetworkingTests.swift create mode 100644 Tests/RootDomainTests.swift create mode 100644 Tests/WorkflowTests.swift diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7fa8411 --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +Carthage/Checkouts + +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output diff --git a/Cartfile b/Cartfile new file mode 100644 index 0000000..9dfa4d9 --- /dev/null +++ b/Cartfile @@ -0,0 +1,3 @@ +github "Moya/Moya" +github "stephencelis/SQLite.swift" ~> 0.11.5 +github "emaloney/CleanroomLogger" ~> 6.0.0 diff --git a/Cartfile.resolved b/Cartfile.resolved new file mode 100644 index 0000000..cf7e898 --- /dev/null +++ b/Cartfile.resolved @@ -0,0 +1,7 @@ +github "Alamofire/Alamofire" "4.8.1" +github "Moya/Moya" "12.0.1" +github "ReactiveCocoa/ReactiveSwift" "4.0.0" +github "ReactiveX/RxSwift" "4.4.0" +github "antitypical/Result" "4.1.0" +github "emaloney/CleanroomLogger" "6.0.2" +github "stephencelis/SQLite.swift" "0.11.5" diff --git a/Contrib/logo.png b/Contrib/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..00d5be16305e9eb9a6b663e8a676a2a8a3fbcecb GIT binary patch literal 5172 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 i{j);Nn>TOX-gy@-g}92$7g$yR0000 + + + + diff --git a/PrivacyMonitorFramework.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/PrivacyMonitorFramework.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/PrivacyMonitorFramework.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/PrivacyMonitorFramework.xcodeproj/xcshareddata/xcschemes/PrivacyMonitorFramework.xcscheme b/PrivacyMonitorFramework.xcodeproj/xcshareddata/xcschemes/PrivacyMonitorFramework.xcscheme new file mode 100644 index 0000000..7e9c1b6 --- /dev/null +++ b/PrivacyMonitorFramework.xcodeproj/xcshareddata/xcschemes/PrivacyMonitorFramework.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PrivacyMonitorFramework.xcodeproj/xcshareddata/xcschemes/PrivacyMonitorFrameworkMac.xcscheme b/PrivacyMonitorFramework.xcodeproj/xcshareddata/xcschemes/PrivacyMonitorFrameworkMac.xcscheme new file mode 100644 index 0000000..7ed6db2 --- /dev/null +++ b/PrivacyMonitorFramework.xcodeproj/xcshareddata/xcschemes/PrivacyMonitorFrameworkMac.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..231a952 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Privacy Monitor - iOS & MacOS Core Frameworks + +[![Privacy Monitor by Osano][logo]][privacymonitor] + +[![MIT License][li]][ll] +[![Platform][pi]][pl] + +This respository and it's dependencies will be automatically imported by [Carthage] when building the Privacy Monitor iOS client and Privacy Monitor Safari plugin. + +You do not need to clone or build this repository manually unless you wish to edit the shared functionality between the applications and are comfortable with modifying Carthage files. + +Instead, you may prefer to follow the instructions in [Privacy Monitor for iOS][ios] or [Privacy Monitor for Safari][safari] respositories. + + +## Export Control + +This distribution includes cryptographic software. The country in which you +currently reside may have restrictions on the import, possession, use, and/or +re-export to another country, of encryption software. BEFORE using any +encryption software, please check your country's laws, regulations and +policies concerning the import, possession, or use, and re-export of encryption +software, to see if this is permitted. See for more +information. + +The U.S. Government Department of Commerce, Bureau of Industry and Security +(BIS), has classified this software as Export Commodity Control Number (ECCN) +5D002.C.1, which includes information security software using or performing +cryptographic functions with asymmetric algorithms. The form and manner of this +Apache Software Foundation distribution makes it eligible for export under the +License Exception ENC Technology Software Unrestricted (TSU) exception (see the +BIS Export Administration Regulations, Section 740.13) for both object code and +source code. + +[privacymonitor]: https://www.privacymonitor.com +[logo]: Contrib/logo.png +[Carthage]: https://github.com/Carthage/Carthage +[osano]: https://www.osano.com +[li]: https://img.shields.io/badge/license-MIT-brightgreen.svg +[ll]: LICENSE +[pi]: https://img.shields.io/badge/platform-ios%20%7C%20mac-lightgrey.svg +[pl]: # +[ios]: https://github.com/osano/privacymonitor-ios +[safari]: https://github.com/osano/privacymonitor-safari diff --git a/Sources/Configs/Configs.swift b/Sources/Configs/Configs.swift new file mode 100644 index 0000000..de06c14 --- /dev/null +++ b/Sources/Configs/Configs.swift @@ -0,0 +1,26 @@ +// +// Configs.swift +// PrivacyMonitorFramework +// +// Created by Christian Roman on 1/9/19. +// Copyright © 2019 Osano. All rights reserved. +// + +import Foundation + +struct Configs { + + struct App { + static let bundleIdentifier = "com.osano.PrivacyMonitorFramework" + } + + struct Network { + static var baseDomain: String { + return "https://api.privacymonitor.com" + } + } +} + +struct Constants { + +} diff --git a/Sources/Helpers/DatabaseManager.swift b/Sources/Helpers/DatabaseManager.swift new file mode 100644 index 0000000..2ef1543 --- /dev/null +++ b/Sources/Helpers/DatabaseManager.swift @@ -0,0 +1,178 @@ +// +// DatabaseManager.swift +// PrivacyMonitor +// +// Created by Christian Roman on 1/10/19. +// Copyright © 2019 Osano. All rights reserved. +// + +import CleanroomLogger +import Foundation +import SQLite + +public class DatabaseManager { + + fileprivate var db: Connection? + fileprivate let table = Table("domains") + + fileprivate let rootDomainColumn = Expression("rootDomain") + fileprivate let nameColumn = Expression("name") + fileprivate let scoreColumn = Expression("score") + fileprivate let previousScoreColumn = Expression("previousScore") + fileprivate let lastVisitedColumn = Expression("lastVisited") + + public static var defaultDatabaseLocation: Connection.Location { + #if os(OSX) + let path = NSSearchPathForDirectoriesInDomains( + .applicationSupportDirectory, .userDomainMask, true + ).first! + "/" + Bundle.main.bundleIdentifier! + + do { + try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) + } + catch {} + + #else + let path = NSSearchPathForDirectoriesInDomains( + .documentDirectory, .userDomainMask, true + ).first! + #endif + return .uri("\(path)/db.sqlite3") + } + + public init(location: Connection.Location = defaultDatabaseLocation) { + do { + db = try Connection(location) + try createTableIfNeeded() + } + catch { + Log.error?.message(error.localizedDescription) + } + } + + private func createTableIfNeeded() throws { + try db?.run(table.create(ifNotExists: true) { t in + t.column(rootDomainColumn, primaryKey: true) + t.column(nameColumn) + t.column(scoreColumn) + t.column(previousScoreColumn) + t.column(lastVisitedColumn, defaultValue: Date()) + }) + } + + func storeURL(withRootDomain rootDomain: String, date: Date = .init()) -> Bool { + do { + let setters = [rootDomainColumn <- rootDomain, + lastVisitedColumn <- date] + + guard let rowId = try db?.run(table.insert(setters)) else { return false } + + return (rowId as NSNumber).boolValue + } + catch { + Log.error?.message(error.localizedDescription) + return false + } + + } + + func storeDomain(_ domain: Domain) -> Bool { + guard let rootDomain = domain.rootDomain else { return false } + + do { + var setters = [rootDomainColumn <- rootDomain, + nameColumn <- domain.name, + scoreColumn <- domain.score, + previousScoreColumn <- domain.previousScore] + if let lastVisited = domain.lastVisited { + setters.append(lastVisitedColumn <- lastVisited) + } + + guard let rowId = try db?.run(table.insert(setters)) else { return false } + + return (rowId as NSNumber).boolValue + } + catch { + Log.error?.message(error.localizedDescription) + return false + } + } + + func retrieve(with rootDomain: String) -> Domain? { + let query = table.filter(rootDomainColumn == rootDomain) + do { + if let domain = try db?.pluck(query) { + let tld = domain[rootDomainColumn] + let name = domain[nameColumn] + let score = domain[scoreColumn] + let previousScore = domain[previousScoreColumn] + let lastVisited = domain[lastVisitedColumn] + + var domain = Domain(rootDomain: tld) + domain.name = name + domain.score = score + domain.previousScore = previousScore + domain.lastVisited = lastVisited + + return domain + } + } + catch { + print(error) + } + + return nil + } + + func update(rootDomain: String, score: Int? = nil, previousScore: Int? = nil, lastVisitedDate: Date = .init()) -> Bool { + let domain = table.filter(rootDomainColumn == rootDomain) + do { + var setters = [lastVisitedColumn <- lastVisitedDate] + + if let score = score { + setters.append(scoreColumn <- score) + } + + if let previousScore = previousScore { + setters.append(previousScoreColumn <- previousScore) + } + + if let count = try db?.run(domain.update(setters)), count > 0 { + return true + } + else { + return false + } + } + catch { + Log.error?.message(error.localizedDescription) + return false + } + } + + func delete(rootDomain: String) -> Bool { + let domain = table.filter(rootDomainColumn == rootDomain) + do { + if let count = try db?.run(domain.delete()), count > 0 { + return true + } + else { + return false + } + } + catch { + print(error) + return false + } + } + + func deleteAll() -> Int { + do { + return try db?.run(table.delete()) ?? 0 + } + catch { + Log.error?.message(error.localizedDescription) + return 0 + } + } +} diff --git a/Sources/Info.plist b/Sources/Info.plist new file mode 100644 index 0000000..1007fd9 --- /dev/null +++ b/Sources/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/Sources/Models/Domain.swift b/Sources/Models/Domain.swift new file mode 100644 index 0000000..343fd14 --- /dev/null +++ b/Sources/Models/Domain.swift @@ -0,0 +1,42 @@ +// +// Domain.swift +// PrivacyMonitor +// +// Created by Christian Roman on 1/9/19. +// Copyright © 2019 Osano. All rights reserved. +// + +import Foundation + +public struct Domain: Decodable { + public var rootDomain: String? + public var score: Int = 0 + public var previousScore: Int = 0 + public var lastVisited: Date? + + public var name: String? + public var domains: [String]? + + enum CodingKeys: String, CodingKey { + case name + case domains + case score + case previousScore + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + name = try? container.decode(String.self, forKey: .name) + domains = try? container.decode([String].self, forKey: .domains) + score = try container.decode(Int.self, forKey: .score) + previousScore = try container.decodeIfPresent(Int.self, forKey: .previousScore) ?? 0 + } + + init(rootDomain: String, score: Int = 0, previousScore: Int = 0, lastVisited: Date? = nil) { + self.rootDomain = rootDomain + self.score = score + self.previousScore = previousScore + self.lastVisited = lastVisited + } +} diff --git a/Sources/Networking/NetworkManager.swift b/Sources/Networking/NetworkManager.swift new file mode 100644 index 0000000..deb5e78 --- /dev/null +++ b/Sources/Networking/NetworkManager.swift @@ -0,0 +1,88 @@ +// +// NetworkManager.swift +// PrivacyMonitorFramework +// +// Created by Christian Roman on 1/9/19. +// Copyright © 2019 Osano. All rights reserved. +// + +import Foundation +import Moya +import Result + +public enum NetworkError: Error { + case unknownError + case invalidRequest + case notFound + case timeOut + case serverError + case decodingError +} + +struct NetworkAdapter { + + static let provider = MoyaProvider() + static let analysisProvider = MoyaProvider() + + static func fetchScore(query: String, previousScore: Int? = nil, completion: @escaping (Result) -> Void) { + provider.request(.getScore(query: query, previousScore: previousScore)) { result in + switch result { + case let .success(response): + do { + _ = try response.filterSuccessfulStatusCodes() + var domain = try JSONDecoder().decode(Domain.self, from: response.data) + domain.rootDomain = query + completion(.success(domain)) + } + catch { + if let moyaError = error as? MoyaError, + let response = moyaError.response { + completion(.failure(errorFromStatusCode(response.statusCode))) + } + else { + completion(.failure(.decodingError)) + } + } + case .failure(_): + completion(.failure(.unknownError)) + } + } + } + + static func requestScoreAnalysis(domain: String, completion: @escaping (Result) -> Void) { + analysisProvider.request(.requestScoreAnalysis(domain: domain)) { result in + switch result { + case let .success(response): + do { + _ = try response.filterSuccessfulStatusCodes() + completion(.success(true)) + } + catch { + if let moyaError = error as? MoyaError, + let response = moyaError.response { + completion(.failure(errorFromStatusCode(response.statusCode))) + } + else { + completion(.failure(.decodingError)) + } + } + case .failure(_): + completion(.failure(.unknownError)) + } + } + } + + static func errorFromStatusCode(_ statusCode: Int) -> NetworkError { + switch statusCode { + case 400: + return .invalidRequest + case 404: + return .notFound + case 500...599: + return .serverError + default: + return .unknownError + } + } + +} diff --git a/Sources/Networking/ServiceError.swift b/Sources/Networking/ServiceError.swift new file mode 100644 index 0000000..d73432b --- /dev/null +++ b/Sources/Networking/ServiceError.swift @@ -0,0 +1,27 @@ +// +// ServiceError.swift +// PrivacyMonitorFramework +// +// Created by Christian Roman on 1/9/19. +// Copyright © 2019 Osano. All rights reserved. +// + +import Foundation + +struct ServiceError: Decodable { + + let status: String + let message: String + + enum CodingKeys: String, CodingKey { + case status + case message + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + status = try container.decode(String.self, forKey: .status) + message = try container.decode(String.self, forKey: .message) + } +} diff --git a/Sources/Networking/Targets/AnalysisService.swift b/Sources/Networking/Targets/AnalysisService.swift new file mode 100644 index 0000000..eece0bd --- /dev/null +++ b/Sources/Networking/Targets/AnalysisService.swift @@ -0,0 +1,44 @@ +// +// AnalysisService.swift +// PrivacyMonitorFramework +// +// Created by Christian Roman on 1/28/19. +// + +import Foundation +import Moya + +enum AnalysisService { + + case requestScoreAnalysis(domain: String) +} + +extension AnalysisService: TargetType { + + var baseURL: URL { + return URL(string: Configs.Network.baseDomain)! + } + + var path: String { + return "analysis" + } + + var method: Moya.Method { + return .post + } + + var sampleData: Data { + return Data() + } + + var task: Task { + switch self { + case let .requestScoreAnalysis(domain): + return .requestParameters(parameters: ["domain": domain], encoding: JSONEncoding.default) + } + } + + var headers: [String: String]? { + return nil + } +} diff --git a/Sources/Networking/Targets/ScoreService.swift b/Sources/Networking/Targets/ScoreService.swift new file mode 100644 index 0000000..82e1fc3 --- /dev/null +++ b/Sources/Networking/Targets/ScoreService.swift @@ -0,0 +1,55 @@ +// +// ScoreService.swift +// PrivacyMonitorFramework +// +// Created by Christian Roman on 1/9/19. +// Copyright © 2019 Osano. All rights reserved. +// + +import Foundation +import Moya + +enum ScoreService { + + case getScore(query: String, previousScore: Int?) +} + +extension ScoreService: TargetType { + + var baseURL: URL { + return URL(string: Configs.Network.baseDomain)! + } + + var path: String { + return "score" + } + + var method: Moya.Method { + return .get + } + + var sampleData: Data { + return Data() + } + + var task: Task { + switch self { + case let .getScore(query, previousScore): + var parameters = [String: String]() + + if let query = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { + parameters["q"] = query + } + + if let previousScore = previousScore { + parameters["p"] = String(previousScore) + } + + return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) + } + } + + var headers: [String: String]? { + return nil + } +} diff --git a/Sources/PrivacyMonitor.swift b/Sources/PrivacyMonitor.swift new file mode 100644 index 0000000..553a83b --- /dev/null +++ b/Sources/PrivacyMonitor.swift @@ -0,0 +1,185 @@ +// +// PrivacyMonitor.swift +// PrivacyMonitorFramework +// +// Created by Christian Roman on 1/13/19. +// + +import CleanroomLogger +import Foundation +import Result + +public enum PrivacyMonitorError: Error { + case invalidURL + case domainDoesNotExist + case databaseError + case underlyingNetworkError(NetworkError) +} + +public struct PrivacyMonitor { + + private let databaseManager: DatabaseManager + + public init(databaseManager: DatabaseManager = .init()) { + Log.enable() + self.databaseManager = databaseManager + } + + public func requestDomainScore(withURL url: URL, visitedDate: Date = .init(), completion: @escaping (Result) -> Void) { + guard let rootDomain = url.rootDomain else { + completion(.failure(.invalidURL)) + return + } + + // Check if domain exists locally + if let domain = databaseManager.retrieve(with: rootDomain) { + // Request domain info from API. + NetworkAdapter.fetchScore(query: rootDomain, previousScore: domain.score) { result in + switch result { + case let .success(domain): + // Domain found, update score in the local database. + if self.databaseManager.update(rootDomain: rootDomain, + score: domain.score, + previousScore: domain.previousScore, + lastVisitedDate: visitedDate), + let domain = self.databaseManager.retrieve(with: rootDomain) { + completion(.success(domain)) + } + else { + Log.error?.message("Error updating \(domain) in local storage") + completion(.failure(.databaseError)) + } + + case let .failure(error): + // Domain not found or network error + switch error { + case .notFound: + completion(.failure(.domainDoesNotExist)) + default: + completion(.failure(.underlyingNetworkError(error))) + } + } + } + } + else { + // Domain not available from local storage, fetch from API and store. + NetworkAdapter.fetchScore(query: rootDomain) { result in + switch result { + case let .success(domain): + // Domain found, store it in the local database. + if self.databaseManager.storeDomain(domain), + let domain = self.databaseManager.retrieve(with: rootDomain) { + completion(.success(domain)) + } + else { + Log.error?.message("Error storing \(domain) in local storage") + completion(.failure(.databaseError)) + } + + case let .failure(error): + // Domain not found or network error + switch error { + case .notFound: + completion(.failure(.domainDoesNotExist)) + default: + completion(.failure(.underlyingNetworkError(error))) + } + } + } + } + } + + public func registerDomainVisit(withURL url: URL, visitedDate: Date = .init(), completion: @escaping (Result) -> Void) { + guard let rootDomain = url.rootDomain else { + completion(.failure(.invalidURL)) + return + } + + // Check if domain exists locally + if let domain = databaseManager.retrieve(with: rootDomain) { + + // Domain available from local storage, check last visited date (>29 days) or score 0 + if let lastVisited = domain.lastVisited, + let daysFromNow = visitedDate.daysFromDate(lastVisited), + daysFromNow > 29 || domain.score == 0 { + + // Request domain info from API. + NetworkAdapter.fetchScore(query: rootDomain, previousScore: domain.score) { result in + switch result { + case let .success(domain): + // Domain found, update score in the local database. + if self.databaseManager.update(rootDomain: rootDomain, + score: domain.score, + previousScore: domain.previousScore, + lastVisitedDate: visitedDate), + let domain = self.databaseManager.retrieve(with: rootDomain) { + completion(.success(domain)) + } + else { + Log.error?.message("Error updating \(domain) in local storage") + completion(.failure(.databaseError)) + } + + case let .failure(error): + // Domain not found or network error + switch error { + case .notFound: + completion(.failure(.domainDoesNotExist)) + default: + completion(.failure(.underlyingNetworkError(error))) + } + } + } + } + else { + // Domain recently visited, let's update its last visited date. + if !databaseManager.update(rootDomain: rootDomain, lastVisitedDate: visitedDate) { + Log.error?.message("Error updating \(domain) in local storage") + } + } + } + else { + // Domain not available from local storage, fetch from API and store. + NetworkAdapter.fetchScore(query: rootDomain) { result in + switch result { + case let .success(domain): + // Domain found, store it in the local database. + if self.databaseManager.storeDomain(domain), + let domain = self.databaseManager.retrieve(with: rootDomain) { + completion(.success(domain)) + } + else { + Log.error?.message("Error storing \(domain) in local storage") + completion(.failure(.databaseError)) + } + + case let .failure(error): + // Domain not found or network error + switch error { + case .notFound: + completion(.failure(.domainDoesNotExist)) + default: + completion(.failure(.underlyingNetworkError(error))) + } + } + } + } + } + + public func requestScoreAnalysis(withURL url: URL, completion: @escaping (Result) -> Void) { + guard let rootDomain = url.rootDomain else { + completion(.failure(.invalidURL)) + return + } + + NetworkAdapter.requestScoreAnalysis(domain: rootDomain) { result in + switch result { + case let .success(success): + completion(.success(success)) + case let .failure(error): + Log.error?.message("Error requesting score analysis for domain: \(rootDomain)") + completion(.failure(.underlyingNetworkError(error))) + } + } + } +} diff --git a/Sources/PrivacyMonitorFramework.h b/Sources/PrivacyMonitorFramework.h new file mode 100644 index 0000000..86e1103 --- /dev/null +++ b/Sources/PrivacyMonitorFramework.h @@ -0,0 +1,11 @@ +// +// PrivacyMonitorFrameworkMobile.h +// PrivacyMonitorFrameworkMobile +// +// Created by Christian Roman on 1/13/19. +// + +@import Foundation; + +FOUNDATION_EXPORT double ExampleFrameworkVersionNumber; +FOUNDATION_EXPORT const unsigned char ExampleFrameworkVersionString[]; diff --git a/Sources/Utils/Extensions/Date+Days.swift b/Sources/Utils/Extensions/Date+Days.swift new file mode 100644 index 0000000..42d837b --- /dev/null +++ b/Sources/Utils/Extensions/Date+Days.swift @@ -0,0 +1,15 @@ +// +// Date+Days.swift +// PrivacyMonitorFramework +// +// Created by Christian Roman on 1/13/19. +// + +import Foundation + +extension Date { + + func daysFromDate(_ date: Date = .init()) -> Int? { + return Calendar.current.dateComponents([.day], from: date, to: self).day + } +} diff --git a/Sources/Utils/Extensions/String+RootDomain.swift b/Sources/Utils/Extensions/String+RootDomain.swift new file mode 100644 index 0000000..2ffaf96 --- /dev/null +++ b/Sources/Utils/Extensions/String+RootDomain.swift @@ -0,0 +1,34 @@ +// +// String+RootDomain.swift +// PrivacyMonitorFramework +// +// Created by Christian Roman on 2/13/19. +// + +import Foundation + +extension String { + + var rootDomain: String? { + var domain = self.lowercased() + let splitArr = domain.split(separator: ".") + let arrLen = splitArr.count + + if arrLen > 2 { + domain = "\(splitArr[arrLen - 2]).\(splitArr[arrLen - 1])" + if (splitArr[arrLen - 2].count == 2 && + splitArr[arrLen - 1].count == 2) || isTopLevelDomain(domain) { + domain = "\(splitArr[arrLen - 3]).\(domain)" + } + } + + return domain + } + + private func isTopLevelDomain(_ domain: String) -> Bool { + let tld = ["edu.ac", "c.se", "d.se", "e.se", "f.se", "g.se", "h.se", "i.se", "k.se", "m.se", "n.se", "o.se", "s.se", "t.se", "u.se", "w.se", "x.se", "y.se", "z.se", "ac.ae", "co.ag", "co.ao", "ed.ao", "gv.ao", "it.ao", "og.ao", "pb.ao", "gv.at", "ac.at", "co.at", "or.at", "id.au", "oz.au", "nt.au", "sa.au", "wa.au", "pp.az", "ac.be", "tv.bo", "am.br", "fm.br", "tv.br", "co.bw", "ab.ca", "bc.ca", "mb.ca", "nb.ca", "nf.ca", "nl.ca", "ns.ca", "nt.ca", "nu.ca", "on.ca", "pe.ca", "qc.ca", "sk.ca", "yk.ca", "co.cc", "co.ck", "ac.cn", "ah.cn", "bj.cn", "cq.cn", "fj.cn", "gd.cn", "gs.cn", "gz.cn", "gx.cn", "ha.cn", "hb.cn", "he.cn", "hi.cn", "hl.cn", "hn.cn", "jl.cn", "js.cn", "jx.cn", "ln.cn", "nm.cn", "nx.cn", "qh.cn", "sc.cn", "sd.cn", "sh.cn", "sn.cn", "sx.cn", "tj.cn", "xj.cn", "xz.cn", "yn.cn", "zj.cn", "us.com", "ac.cr", "co.cr", "ed.cr", "fi.cr", "go.cr", "or.cr", "sa.cr", "tm.cy", "ac.cy", "ac.fj", "co.fk", "ac.fk", "tm.fr", "co.gg", "ac.gn", "iz.hr", "co.hu", "tm.hu", "ac.id", "co.id", "or.id", "go.id", "ac.il", "co.il", "co.im", "ac.im", "co.in", "ac.in", "ac.ir", "co.ir", "co.je", "ac.jp", "ad.jp", "co.jp", "ed.jp", "go.jp", "gr.jp", "lg.jp", "ne.jp", "or.jp", "co.kr", "or.kr", "co.ls", "id.lv", "id.ly", "co.ma", "tm.mc", "tm.mg", "co.mu", "ac.mw", "co.mw", "ac.nz", "co.nz", "co.om", "ac.com", "ac.pa", "tm.ro", "nt.ro", "pp.ru", "ac.ru", "ac.rw", "co.rw", "tv.sd", "pp.se", "tm.se", "fh.se", "ab.se", "ac.se", "bd.se", "ac.th", "co.th", "in.th", "go.th", "mi.th", "or.th", "ac.tj", "co.tj", "go.tj", "av.tr", "dr.tr", "co.tt", "co.tz", "ac.tz", "go.tz", "or.tz", "ne.tz", "ck.ua", "cn.ua", "cv.ua", "dp.ua", "dn.ua", "if.ua", "kh.ua", "ks.ua", "km.ua", "kv.ua", "kr.ua", "lg.ua", "mk.ua", "od.ua", "pl.ua", "rv.ua", "te.ua", "vn.ua", "zp.ua", "zt.ua", "co.ug", "ac.ug", "sc.ug", "go.ug", "ne.ug", "or.ug", "ac.uk", "co.uk", "me.uk", "bl.uk", "ak.us", "al.us", "ar.us", "az.us", "ca.us", "co.us", "ct.us", "dc.us", "de.us", "fl.us", "ga.us", "hi.us", "ia.us", "id.us", "il.us", "in.us", "ks.us", "ky.us", "la.us", "ma.us", "md.us", "me.us", "mi.us", "mn.us", "mo.us", "ms.us", "mt.us", "nc.us", "nd.us", "ne.us", "nh.us", "nj.us", "nm.us", "nv.us", "ny.us", "oh.us", "ok.us", "or.us", "pa.us", "ri.us", "sc.us", "sd.us", "tn.us", "tx.us", "ut.us", "vt.us", "va.us", "wa.us", "wi.us", "wv.us", "wy.us", "co.ve", "ac.vn", "ac.yu", "co.yu", "ac.za", "co.za", "tm.za", "co.zm", "ac.zm", "co.zw", "ac.zw", "gov.ac", "net.ac", "mil.ac", "org.ac", "nom.ad", "net.ae", "gov.ae", "org.ae", "mil.ae", "sch.ae", "pro.ae", "gov.af", "edu.af", "net.af", "com.af", "com.ag", "org.ag", "net.ag", "nom.ag", "off.ai", "com.ai", "net.ai", "org.ai", "gov.al", "edu.al", "org.al", "com.al", "net.al", "upt.al", "com.an", "net.an", "org.an", "edu.an", "com.ar", "gov.ar", "int.ar", "mil.ar", "net.ar", "org.ar", "ip6.arpa", "uri.arpa", "urn.arpa", "asn.au", "com.au", "net.au", "org.au", "act.au", "nsw.au", "qld.au", "tas.au", "vic.au", "gov.au", "edu.au:", "com.aw", "com.az", "net.az", "int.az", "gov.az", "biz.az", "org.az", "edu.az", "mil.az", "com.bb", "edu.bb", "gov.bb", "net.bb", "org.bb", "com.bd", "edu.bd", "net.bd", "gov.bd", "org.bd", "mil.bd", "gov.bf", "com.bm", "edu.bm", "org.bm", "gov.bm", "net.bm", "com.bn", "edu.bn", "org.bn", "net.bn", "com.bo", "org.bo", "net.bo", "gov.bo", "gob.bo", "edu.bo", "mil.bo", "int.bo", "agr.br", "art.br", "edu.br", "com.br", "esp.br", "far.br", "g12.br", "gov.br", "imb.br", "ind.br", "inf.br", "mil.br", "net.br", "org.br", "psi.br", "rec.br", "srv.br", "tmp.br", "tur.br", "etc.br", "adm.br", "adv.br", "arq.br", "ato.br", "bio.br", "bmd.br", "cim.br", "cng.br", "cnt.br", "ecn.br", "eng.br", "eti.br", "fnd.br", "fot.br", "fst.br", "ggf.br", "jor.br", "lel.br", "mat.br", "med.br", "mus.br", "not.br", "ntr.br", "odo.br", "ppg.br", "pro.br", "psc.br", "qsl.br", "slg.br", "trd.br", "vet.br", "zlg.br", "dpn.br", "nom.br", "com.bs", "net.bs", "org.bs", "com.bt", "edu.bt", "gov.bt", "net.bt", "org.bt", "org.bw", "gov.by", "mil.by", "com.cd", "net.cd", "org.cd", "com.ch", "net.ch", "org.ch", "gov.ch", "com.cn", "edu.cn", "gov.cn", "net.cn", "org.cn", "com.co", "edu.co", "org.co", "gov.co", "mil.co", "net.co", "nom.co", "com.cu", "edu.cu", "org.cu", "net.cu", "gov.cu", "inf.cu", "gov.cx", "com.cy", "biz.cy", "ltd.cy", "pro.cy", "net.cy", "org.cy", "com.dm", "net.dm", "org.dm", "edu.dm", "gov.dm", "edu.do", "gov.do", "gob.do", "com.do", "org.do", "sld.do", "web.do", "net.do", "mil.do", "art.do", "com.dz", "org.dz", "net.dz", "gov.dz", "edu.dz", "pol.dz", "art.dz", "com.ec", "net.ec", "fin.ec", "med.ec", "pro.ec", "org.ec", "edu.ec", "gov.ec", "mil.ec", "com.ee", "org.ee", "fie.ee", "pri.ee", "eun.eg", "edu.eg", "sci.eg", "gov.eg", "com.eg", "org.eg", "net.eg", "mil.eg", "com.es", "nom.es", "org.es", "gob.es", "edu.es", "com.et", "gov.et", "org.et", "edu.et", "net.et", "biz.et", "biz.fj", "com.fj", "net.fj", "org.fj", "pro.fj", "gov.fj", "mil.fj", "org.fk", "gov.fk", "nom.fk", "net.fk", "nom.fr", "prd.fr", "com.fr", "com.ge", "edu.ge", "gov.ge", "org.ge", "mil.ge", "net.ge", "pvt.ge", "net.gg", "org.gg", "com.gh", "edu.gh", "gov.gh", "org.gh", "mil.gh", "com.gi", "ltd.gi", "gov.gi", "mod.gi", "edu.gi", "org.gi", "com.gn", "gov.gn", "org.gn", "net.gn", "com.gp", "net.gp", "edu.gp", "org.gp", "com.gr", "edu.gr", "net.gr", "org.gr", "gov.gr", "com.hk", "edu.hk", "gov.hk", "idv.hk", "net.hk", "org.hk", "com.hn", "edu.hn", "org.hn", "net.hn", "mil.hn", "gob.hn", "com.hr", "com.ht", "net.ht", "pro.ht", "org.ht", "art.ht", "pol.ht", "rel.ht", "med.ht", "edu.ht", "org.hu", "sex.hu", "gov.ie", "org.il", "net.il", "k12.il", "gov.il", "idf.il", "ltd.co.im", "plc.co.im", "net.im", "gov.im", "org.im", "nic.im", "net.in", "org.in", "gen.in", "ind.in", "nic.in", "edu.in", "res.in", "gov.in", "mil.in", "gov.ir", "net.ir", "org.ir", "sch.ir", "gov.it", "net.je", "org.je", "edu.jm", "gov.jm", "com.jm", "net.jm", "org.jm", "com.jo", "org.jo", "net.jo", "edu.jo", "gov.jo", "mil.jo", "mie.jp", "per.kh", "com.kh", "edu.kh", "gov.kh", "mil.kh", "net.kh", "org.kh", "com.kw", "edu.kw", "gov.kw", "net.kw", "org.kw", "mil.kw", "edu.ky", "gov.ky", "com.ky", "org.ky", "net.ky", "org.kz", "edu.kz", "net.kz", "gov.kz", "mil.kz", "com.kz", "net.lb", "org.lb", "gov.lb", "edu.lb", "com.lb", "com.lc", "org.lc", "edu.lc", "gov.lc", "com.li", "net.li", "org.li", "gov.li", "gov.lk", "sch.lk", "net.lk", "int.lk", "com.lk", "org.lk", "edu.lk", "ngo.lk", "soc.lk", "web.lk", "ltd.lk", "grp.lk", "com.lr", "edu.lr", "gov.lr", "org.lr", "net.lr", "org.ls", "gov.lt", "mil.lt", "gov.lu", "mil.lu", "org.lu", "net.lu", "com.lv", "edu.lv", "gov.lv", "org.lv", "mil.lv", "net.lv", "asn.lv", "com.ly", "net.ly", "gov.ly", "plc.ly", "edu.ly", "sch.ly", "med.ly", "org.ly", "net.ma", "gov.ma", "org.ma", "org.mg", "nom.mg", "gov.mg", "prd.mg", "com.mg", "edu.mg", "mil.mg", "com.mk", "org.mk", "com.mo", "net.mo", "org.mo", "edu.mo", "gov.mo", "org.mt", "com.mt", "gov.mt", "edu.mt", "net.mt", "com.mu", "biz.mv", "com.mv", "edu.mv", "gov.mv", "int.mv", "mil.mv", "net.mv", "org.mv", "pro.mv", "com.mw", "edu.mw", "gov.mw", "int.mw", "net.mw", "org.mw", "com.mx", "net.mx", "org.mx", "edu.mx", "gob.mx", "com.my", "net.my", "org.my", "gov.my", "edu.my", "mil.my", "edu.ng", "com.ng", "gov.ng", "org.ng", "net.ng", "gob.ni", "com.ni", "edu.ni", "org.ni", "nom.ni", "net.ni", "mil.no", "vgs.no", "fhs.no", "com.np", "org.np", "edu.np", "net.np", "gov.np", "mil.np", "gov.nr", "edu.nr", "biz.nr", "org.nr", "com.nr", "net.nr", "cri.nz", "gen.nz", "iwi.nz", "mil.nz", "net.nz", "org.nz", "com.om", "edu.om", "sch.om", "gov.om", "net.om", "org.om", "mil.om", "biz.om", "pro.om", "med.om", "com.pa", "sld.pa", "gob.pa", "edu.pa", "org.pa", "net.pa", "abo.pa", "ing.pa", "med.pa", "nom.pa", "com.pe", "org.pe", "net.pe", "edu.pe", "mil.pe", "gob.pe", "nom.pe", "com.pf", "org.pf", "edu.pf", "com.pg", "net.pg", "com.ph", "gov.ph", "com.pk", "net.pk", "edu.pk", "org.pk", "fam.pk", "biz.pk", "web.pk", "gov.pk", "gob.pk", "gok.pk", "gon.pk", "gop.pk", "gos.pk", "com.pl", "biz.pl", "net.pl", "art.pl", "edu.pl", "org.pl", "ngo.pl", "gov.pl", "mil.pl", "waw.pl", "gda.pl", "biz.pr", "com.pr", "edu.pr", "gov.pr", "net.pr", "org.pr", "pro.pr", "law.pro", "med.pro", "cpa.pro", "edu.ps", "gov.ps", "sec.ps", "plo.ps", "com.ps", "org.ps", "net.ps", "com.pt", "edu.pt", "gov.pt", "int.pt", "net.pt", "org.pt", "net.py", "org.py", "gov.py", "edu.py", "com.py", "com.ro", "org.ro", "nom.ro", "rec.ro", "www.ro", "com.ru", "net.ru", "org.ru", "msk.ru", "int.ru", "gov.rw", "net.rw", "edu.rw", "com.rw", "int.rw", "mil.rw", "gov.rw", "com.sa", "edu.sa", "sch.sa", "med.sa", "gov.sa", "net.sa", "org.sa", "pub.sa", "com.sb", "gov.sb", "net.sb", "edu.sb", "com.sc", "gov.sc", "net.sc", "org.sc", "edu.sc", "com.sd", "net.sd", "org.sd", "edu.sd", "med.sd", "gov.sd", "org.se", "fhv.se", "mil.se", "com.sg", "net.sg", "org.sg", "gov.sg", "edu.sg", "per.sg", "idn.sg", "edu.sv", "com.sv", "gob.sv", "org.sv", "red.sv", "gov.sy", "com.sy", "net.sy", "net.th", "biz.tj", "com.tj", "edu.tj", "int.tj", "net.tj", "org.tj", "web.tj", "gov.tj", "mil.tj", "com.tn", "gov.tn", "org.tn", "ind.tn", "nat.tn", "ens.tn", "fin.tn", "net.tn", "gov.to", "gov.tp", "com.tr", "biz.tr", "net.tr", "org.tr", "web.tr", "gen.tr", "bbs.tr", "tel.tr", "gov.tr", "bel.tr", "pol.tr", "mil.tr", "k12.tr", "edu.tr", "com.tt", "org.tt", "net.tt", "biz.tt", "pro.tt", "edu.tt", "gov.tt", "gov.tv", "edu.tw", "gov.tw", "mil.tw", "com.tw", "net.tw", "org.tw", "idv.tw", "com.ua", "gov.ua", "net.ua", "edu.ua", "org.ua", "gov.uk", "ltd.uk", "mil.uk", "mod.uk", "net.uk", "nic.uk", "nhs.uk", "org.uk", "plc.uk", "sch.uk", "jet.uk", "nel.uk", "nls.uk", "sch.uk", "dni.us", "fed.us", "isa.us", "nsn.us", "edu.uy", "gub.uy", "org.uy", "com.uy", "net.uy", "mil.uy", "com.ve", "net.ve", "org.ve", "web.ve", "com.vi", "org.vi", "edu.vi", "gov.vi", "com.vn", "net.vn", "org.vn", "edu.vn", "gov.vn", "int.vn", "biz.vn", "pro.vn", "com.ye", "net.ye", "org.yu", "edu.yu", "edu.za", "gov.za", "law.za", "mil.za", "nom.za", "org.za", "alt.za", "net.za", "ngo.za", "web.za", "org.zm", "gov.zm", "sch.zm", "org.zw", "gov.zw", "name.ae", "e164.arpa", "iris.arpa", "priv.at", "info.au", "conf.au", "name.az", "info.az", "coop.br", "info.cy", "name.cy", "asso.dz", "info.ec", "name.et", "info.et", "info.fj", "name.fj", "asso.fr", "gouv.fr", "asso.gp", "from.hr", "name.hr", "firm.ht", "shop.ht", "info.ht", "asso.ht", "coop.ht", "gouv.ht", "info.hu", "priv.hu", "2000.hu", "bolt.hu", "city.hu", "film.hu", "news.hu", "shop.hu", "suli.hu", "szex.hu", "muni.il", "firm.in", "gifu.jp", "nara.jp", "saga.jp", "oita.jp", "kobe.jp", "assn.lk", "conf.lv", "asso.mc", "army.mil", "navy.mil", "aero.mv", "coop.mv", "info.mv", "name.mv", "coop.mw", "name.my", "stat.no", "priv.no", "info.nr", "geek.nz", "govt.nz", "info.pl", "wroc.pl", "lodz.pl", "info.pr", "isla.pr", "name.pr", "nome.pt", "publ.pt", "info.ro", "arts.ro", "firm.ro", "info.sd", "sshn.se", "fhsk.se", "name.tj", "intl.tn", "info.tn", "info.tr", "name.tr", "info.tt", "name.tt", "game.tw", "ebiz.tw", "club.tw", "kiev.ua", "lviv.ua", "sumy.ua", "kids.us", "info.ve", "info.vn", "name.vn", "city.za", "uniti.al", "soros.al", "inima.al", "csiro.au", "press.cy", "aland.fi", "adult.ht", "perso.ht", "sport.hu", "agrar.hu", "forum.hu", "games.hu", "hotel.hu", "lakas.hu", "media.hu", "video.hu", "iwate.jp", "akita.jp", "gunma.jp", "chiba.jp", "tokyo.jp", "fukui.jp", "aichi.jp", "shiga.jp", "kyoto.jp", "osaka.jp", "hyogo.jp", "ehime.jp", "kochi.jp", "hotel.lk", "music.mobi", "herad.no", "maori.nz", "store.ro", "brand.se", "parti.se", "press.se", "lutsk.ua", "rovno.ua", "icnet.uk", "tirana.al", "school.fj", "presse.fr", "casino.hu", "jogasz.hu", "reklam.hu", "tozsde.hu", "utazas.hu", "aomori.jp", "miyagi.jp", "toyama.jp", "nagano.jp", "kagawa.jp", "sendai.jp", "nagoya.jp", "museum.mv", "museum.mw", "museum.no", "idrett.no", "school.nz", "museum.om", "krakow.pl", "poznan.pl", "gdansk.pl", "slupsk.pl", "lublin.pl", "komvux.se", "lanarb.se", "lanbib.se", "crimea.ua", "odessa.ua", "police.uk", "health.vn", "school.za", "in-addr.arpa", "ekloges.cy", "erotica.hu", "erotika.hu", "ibaraki.jp", "tochigi.jp", "saitama.jp", "niigata.jp", "tottori.jp", "shimane.jp", "okayama.jp", "fukuoka.jp", "okinawa.jp", "sapporo.jp", "weather.mobi", "kommune.no", "wroclaw.pl", "olsztyn.pl.torun.pl", "komforb.se", "tourism.tn", "donetsk.ua", "kharkov.ua", "kherson.ua", "lugansk.ua", "poltava.ua", "vinnica.ua", "vatican.va", "ingatlan.hu", "konyvelo.hu", "hokkaido.jp", "yamagata.jp", "kanagawa.jp", "ishikawa.jp", "shizuoka.jp", "wakayama.jp", "nagasaki.jp", "kumamoto.jp", "miyazaki.jp", "yokohama.jp", "kawasaki.jp", "warszawa.pl", "szczecin.pl", "nikolaev.ua", "ternopil.ua", "uzhgorod.ua", "zhitomir.ua", "fukushima.jp", "yamanashi.jp", "hiroshima.jp", "yamaguchi.jp", "tokushima.jp", "kagoshima.jp", "folkebibl.no", "bialystok.pl", "cherkassy.ua", "chernigov.ua", "parliament.cy", "kitakyushu.jp", "fylkesbibl.no", "chernovtsy.ua", "kirovograd.ua", "sebastopol.ua", "parliament.uk", "zaporizhzhe.ua", "khmelnitskiy.ua", "naturbruksgymn.se", "dnepropetrovsk.ua", "kommunalforbund.se", "ivano-frankivsk.ua", "british-library.uk", "national-library-scotland.uk", "uk.com" + ] + + return tld.contains(domain) + } +} diff --git a/Sources/Utils/Extensions/URL+RootDomain.swift b/Sources/Utils/Extensions/URL+RootDomain.swift new file mode 100644 index 0000000..74208e5 --- /dev/null +++ b/Sources/Utils/Extensions/URL+RootDomain.swift @@ -0,0 +1,17 @@ +// +// URL+RootDomain.swift +// PrivacyMonitorFramework +// +// Created by Christian Roman on 1/10/19. +// Copyright © 2019 Osano. All rights reserved. +// + +import Foundation + +public extension URL { + + var rootDomain: String? { + guard let host = host else { return nil } + return host.rootDomain + } +} diff --git a/Tests/DatabaseTests.swift b/Tests/DatabaseTests.swift new file mode 100644 index 0000000..48aebcd --- /dev/null +++ b/Tests/DatabaseTests.swift @@ -0,0 +1,78 @@ +// +// DatabaseTests.swift +// PrivacyMonitorFramework +// +// Created by Christian Roman on 1/14/19. +// + +import XCTest +import SQLite +@testable import PrivacyMonitorFramework + +class DatabaseTests: XCTestCase { + + fileprivate let dbManager = DatabaseManager(location: .inMemory) + fileprivate let domain = Domain(rootDomain: "youtube.com", score: 722) + + func testStoreRetrieveDomain() { + XCTAssertTrue(dbManager.storeDomain(domain)) + + let retrievedDomain = dbManager.retrieve(with: "youtube.com") + + XCTAssertEqual(retrievedDomain?.rootDomain, "youtube.com") + XCTAssertEqual(retrievedDomain?.score, 722) + } + + func testStoreRetrieveDomainWithDate() { + let domain = Domain(rootDomain: "google.com", score: 100, lastVisited: Date(timeIntervalSince1970: 5000)) + + XCTAssertTrue(dbManager.storeDomain(domain)) + + let retrievedDomain = dbManager.retrieve(with: "google.com") + + XCTAssertEqual(retrievedDomain?.rootDomain, "google.com") + XCTAssertEqual(retrievedDomain?.score, 100) + XCTAssertEqual(retrievedDomain?.lastVisited, Date(timeIntervalSince1970: 5000)) + } + + func testDuplicateDomain() { + let duplicate = Domain(rootDomain: "youtube.com", score: 722) + let duplicateDifferent = Domain(rootDomain: "youtube.com", score: 999) + + XCTAssertTrue(dbManager.storeDomain(domain)) + XCTAssertFalse(dbManager.storeDomain(duplicate)) + XCTAssertFalse(dbManager.storeDomain(duplicateDifferent)) + } + + func testUpdateDomainScore() { + XCTAssertTrue(dbManager.storeDomain(domain)) + + XCTAssertTrue(dbManager.update(rootDomain: "youtube.com", score: 999)) + + let retrievedDomain = dbManager.retrieve(with: "youtube.com") + XCTAssertEqual(retrievedDomain?.score, 999) + } + + func testDeleteDomain() { + XCTAssertTrue(dbManager.storeDomain(domain)) + + XCTAssertTrue(dbManager.delete(rootDomain: "youtube.com")) + + let retrievedDomain = dbManager.retrieve(with: "youtube.com") + XCTAssertNil(retrievedDomain) + } + + func testDeleteAll() { + let domains = [Domain(rootDomain: "youtube.com", score: 299), + Domain(rootDomain: "youtu.be", score: 299), + Domain(rootDomain: "amazon.com", score: 350), + Domain(rootDomain: "amazon.co.uk", score: 351), + Domain(rootDomain: "google.com", score: 999)] + + for domain in domains { + XCTAssertTrue(dbManager.storeDomain(domain)) + } + + XCTAssertEqual(dbManager.deleteAll(), domains.count) + } +} diff --git a/Tests/Info.plist b/Tests/Info.plist new file mode 100644 index 0000000..6c40a6c --- /dev/null +++ b/Tests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Tests/NetworkingTests.swift b/Tests/NetworkingTests.swift new file mode 100644 index 0000000..87ee1ab --- /dev/null +++ b/Tests/NetworkingTests.swift @@ -0,0 +1,133 @@ +// +// NetworkingTests.swift +// PrivacyMonitorFramework +// +// Created by Christian Roman on 1/14/19. +// + +import XCTest +@testable import PrivacyMonitorFramework + +class NetworkingTests: XCTestCase { + + func testDomainParsing() { + let expect = expectation(description: "Expect initial load and a patch to happen") + + NetworkAdapter.fetchScore(query: "youtube.com") { result in + switch result { + case let .success(domain): + XCTAssertNotNil(domain) + XCTAssertEqual(domain.name, "YouTube") + XCTAssertEqual(domain.domains, ["youtube.com", "youtu.be"]) + XCTAssertEqual(domain.previousScore, 755) + XCTAssertEqual(domain.score, 722) + + case .failure(_): + XCTFail() + } + + expect.fulfill() + } + + waitForExpectations(timeout: 20.0, handler: nil) + } + + func testDomainParsingWithPreviousScore() { + let expect = expectation(description: "Expect initial load and a patch to happen") + + NetworkAdapter.fetchScore(query: "youtube.com", previousScore: 999) { result in + switch result { + case let .success(domain): + XCTAssertNotNil(domain) + XCTAssertEqual(domain.name, "YouTube") + XCTAssertEqual(domain.domains, ["youtube.com", "youtu.be"]) + XCTAssertEqual(domain.previousScore, 755) + XCTAssertEqual(domain.score, 722) + + case .failure(_): + XCTFail() + } + + expect.fulfill() + } + + waitForExpectations(timeout: 20.0, handler: nil) + } + + func testDomainParsingWithNoPreviousScore() { + let expect = expectation(description: "Expect initial load and a patch to happen") + + NetworkAdapter.fetchScore(query: "webex.com") { result in + switch result { + case let .success(domain): + XCTAssertNotNil(domain) + XCTAssertEqual(domain.name, "Cisco Webex Teams") + XCTAssertEqual(domain.score, 722) + XCTAssertEqual(domain.previousScore, 0) + + case .failure(_): + XCTFail() + } + + expect.fulfill() + } + + waitForExpectations(timeout: 20.0, handler: nil) + } + + func testDomainNotFound() { + let expect = expectation(description: "Expect initial load and a patch to happen") + + NetworkAdapter.fetchScore(query: "facebook.com") { result in + switch result { + case .success(_): + XCTFail() + + case let .failure(error): + XCTAssertNotNil(error) + XCTAssertEqual(error, NetworkError.notFound) + } + + expect.fulfill() + } + + waitForExpectations(timeout: 20.0, handler: nil) + } + + func testInvalidRequest() { + let expect = expectation(description: "Expect initial load and a patch to happen") + + NetworkAdapter.fetchScore(query: "") { result in + switch result { + case .success(_): + XCTFail() + + case let .failure(error): + XCTAssertNotNil(error) + XCTAssertEqual(error, NetworkError.invalidRequest) + } + + expect.fulfill() + } + + waitForExpectations(timeout: 20.0, handler: nil) + } + + func testRequestScoreAnalysis() { + let expect = expectation(description: "Expect initial load and a patch to happen") + + NetworkAdapter.requestScoreAnalysis(domain: "microsoft.com") { result in + switch result { + case let .success(success): + XCTAssertTrue(success) + case .failure(_): + XCTFail() + } + + expect.fulfill() + } + + waitForExpectations(timeout: 20.0, handler: nil) + } + +} diff --git a/Tests/RootDomainTests.swift b/Tests/RootDomainTests.swift new file mode 100644 index 0000000..1387d93 --- /dev/null +++ b/Tests/RootDomainTests.swift @@ -0,0 +1,49 @@ +// +// RootDomainTests.swift +// PrivacyMonitorFramework +// +// Created by Christian Roman on 1/14/19. +// + +import XCTest +@testable import PrivacyMonitorFramework + +class RootDomainTests: XCTestCase { + + func testRootDomainExtraction() { + XCTAssertEqual("some.subdomain.google.co.uk".rootDomain, "google.co.uk") + XCTAssertEqual("x.y.z.a.b.blog.osano.com".rootDomain, "osano.com") + XCTAssertEqual("www.google.co.uk".rootDomain, "google.co.uk") + XCTAssertEqual("google.co.uk".rootDomain, "google.co.uk") + XCTAssertEqual("www.google.com".rootDomain, "google.com") + XCTAssertEqual("amazon.co.uk".rootDomain, "amazon.co.uk") + XCTAssertEqual("www.amazon.co.uk".rootDomain, "amazon.co.uk") + XCTAssertEqual("www.youtube.com".rootDomain, "youtube.com") + XCTAssertEqual("m.youtube.com".rootDomain, "youtube.com") + XCTAssertEqual("youtube.com".rootDomain, "youtube.com") + XCTAssertEqual("www.joomla.subdomain.php.net".rootDomain, "php.net") + XCTAssertEqual("foo.bar.com".rootDomain, "bar.com") + XCTAssertEqual("foo.ca".rootDomain, "foo.ca") + XCTAssertEqual("foo.bar.ca".rootDomain, "bar.ca") + XCTAssertEqual("foo.blogspot.com".rootDomain, "blogspot.com") + XCTAssertEqual("foo.blogspot.co.uk".rootDomain, "blogspot.co.uk") + XCTAssertEqual("foo.uk.com".rootDomain, "foo.uk.com") + XCTAssertEqual("state.CA.us".rootDomain, "state.ca.us") + XCTAssertEqual("www.state.pa.us".rootDomain, "state.pa.us") + XCTAssertEqual("pvt.k12.ca.us".rootDomain, "k12.ca.us") + XCTAssertEqual("www4.yahoo.co.uk".rootDomain, "yahoo.co.uk") + XCTAssertEqual("home.netscape.com".rootDomain, "netscape.com") + XCTAssertEqual("web.MIT.edu".rootDomain, "mit.edu") + XCTAssertEqual("foo.eDu.au".rootDomain, "edu.au") + XCTAssertEqual("utenti.blah.IT".rootDomain, "blah.it") + XCTAssertEqual("dominio.com.co".rootDomain, "dominio.com.co") + XCTAssertEqual("www.microsoft.com".rootDomain, "microsoft.com") + XCTAssertEqual("msdn.microsoft.com".rootDomain, "microsoft.com") + } + + func testRootDomainExtractionFromURL() { + let url = URL(string: "https://www.youtube.com") + + XCTAssertEqual(url?.rootDomain, "youtube.com") + } +} diff --git a/Tests/WorkflowTests.swift b/Tests/WorkflowTests.swift new file mode 100644 index 0000000..bf34a6a --- /dev/null +++ b/Tests/WorkflowTests.swift @@ -0,0 +1,185 @@ +// +// WorkflowTests.swift +// PrivacyMonitorFramework +// +// Created by Christian Roman on 1/14/19. +// + +import XCTest +@testable import PrivacyMonitorFramework + +class WorkflowTests: XCTestCase { + + fileprivate let dbManager = DatabaseManager(location: .inMemory) + fileprivate var privacyMonitor: PrivacyMonitor? + + override func setUp() { + super.setUp() + + privacyMonitor = PrivacyMonitor(databaseManager: dbManager) + } + + func testManuallyURLVisit() { + guard let url = URL(string: "https://youtube.com"), + let privacyMonitor = privacyMonitor else { + XCTFail() + return + } + + let expect = expectation(description: "Expect initial load and a patch to happen") + + privacyMonitor.requestDomainScore(withURL: url) { result in + switch result { + case let .success(domain): + XCTAssertNotNil(domain) + XCTAssertEqual(domain.rootDomain, "youtube.com") + XCTAssertEqual(domain.name, "YouTube") + XCTAssertEqual(domain.score, 722) + XCTAssertEqual(domain.previousScore, 755) + + case .failure(_): + XCTFail() + } + + expect.fulfill() + } + + waitForExpectations(timeout: 20.0, handler: nil) + } + + func testBasicWorkflow() { + guard let url = URL(string: "https://www.youtube.com") else { + XCTFail() + return + } + + let expect = expectation(description: "Expect initial load and a patch to happen") + + privacyMonitor?.registerDomainVisit(withURL: url) { result in + switch result { + case let .success(domain): + XCTAssertNotNil(domain) + XCTAssertEqual(domain.rootDomain, "youtube.com") + XCTAssertEqual(domain.name, "YouTube") + XCTAssertEqual(domain.score, 722) + XCTAssertEqual(domain.previousScore, 755) + + case .failure(_): + XCTFail() + } + + expect.fulfill() + } + + waitForExpectations(timeout: 20.0, handler: nil) + } + + func testWorkflowFromExistingDomain() { + guard let url = URL(string: "https://www.youtube.com") else { + XCTFail() + return + } + + var domain = Domain(rootDomain: "youtube.com", score: 500) + domain.lastVisited = Date(timeIntervalSince1970: 5000) + + XCTAssertTrue(dbManager.storeDomain(domain)) + + let expect = expectation(description: "Expect initial load and a patch to happen") + expect.isInverted = true + + privacyMonitor?.registerDomainVisit(withURL: url, visitedDate: Date(timeIntervalSince1970: 5010)) { result in + switch result { + case let .success(domain): + XCTAssertNotNil(domain) + XCTAssertEqual(domain.rootDomain, "youtube.com") + XCTAssertEqual(domain.score, 500) + XCTAssertEqual(domain.previousScore, 0) + XCTAssertEqual(domain.lastVisited, Date(timeIntervalSince1970: 5010)) + + case .failure(_): + XCTFail() + } + + expect.fulfill() + } + + waitForExpectations(timeout: 20.0, handler: nil) + } + + func testWorkflowFromExistingDomainMonthLater() { + guard let url = URL(string: "https://www.youtube.com") else { + XCTFail() + return + } + + let lastVisited = Date(timeIntervalSince1970: 5000) + + var domain = Domain(rootDomain: "youtube.com", score: 500) + domain.lastVisited = lastVisited + + let monthLater = Calendar.current.date(byAdding: .month, value: 1, to: lastVisited)! + + XCTAssertTrue(dbManager.storeDomain(domain)) + + let expect = expectation(description: "Expect initial load and a patch to happen") + + privacyMonitor?.registerDomainVisit(withURL: url, visitedDate: monthLater) { result in + switch result { + case let .success(domain): + XCTAssertNotNil(domain) + XCTAssertEqual(domain.rootDomain, "youtube.com") + XCTAssertEqual(domain.score, 722) + XCTAssertEqual(domain.previousScore, 755) + XCTAssertEqual(domain.lastVisited, monthLater) + + case .failure(_): + XCTFail() + } + + expect.fulfill() + } + + waitForExpectations(timeout: 20.0, handler: nil) + } + + func testWorkflowFromExistingDomainMonthLaterNoDateParameter() { + guard let url = URL(string: "https://www.youtube.com") else { + XCTFail() + return + } + + let lastVisited = Calendar.current.date(byAdding: .month, value: -2, to: Date())! + + var domain = Domain(rootDomain: "youtube.com", score: 500) + domain.lastVisited = lastVisited + + XCTAssertTrue(dbManager.storeDomain(domain)) + + let expect = expectation(description: "Expect initial load and a patch to happen") + + privacyMonitor?.registerDomainVisit(withURL: url) { result in + switch result { + case let .success(domain): + XCTAssertNotNil(domain) + XCTAssertEqual(domain.rootDomain, "youtube.com") + XCTAssertEqual(domain.score, 722) + XCTAssertEqual(domain.previousScore, 755) + + guard let domainLastVisited = domain.lastVisited else { + XCTFail() + return + } + + XCTAssertGreaterThan(domainLastVisited, lastVisited) + + case .failure(_): + XCTFail() + } + + expect.fulfill() + } + + waitForExpectations(timeout: 20.0, handler: nil) + } +}