diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..330d167 --- /dev/null +++ b/.gitignore @@ -0,0 +1,90 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.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 +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.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/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# 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 + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ diff --git a/Pi-Helper.xcodeproj/project.pbxproj b/Pi-Helper.xcodeproj/project.pbxproj index 34cfc4c..a162d2b 100644 --- a/Pi-Helper.xcodeproj/project.pbxproj +++ b/Pi-Helper.xcodeproj/project.pbxproj @@ -22,6 +22,9 @@ 282126AE235BF21D00072D52 /* AddPiHoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282126AD235BF21D00072D52 /* AddPiHoleView.swift */; }; 282126B0235BF52F00072D52 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282126AF235BF52F00072D52 /* ActivityIndicatorView.swift */; }; 282126B4235C0F5400072D52 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 282126B6235C0F5400072D52 /* Localizable.strings */; }; + 28D72051238067F30038D439 /* libresolv.9.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 28D72050238067EC0038D439 /* libresolv.9.tbd */; }; + 28D7205923808E8C0038D439 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D7205823808E8C0038D439 /* Extensions.swift */; }; + 28D7205C2380C2090038D439 /* resolver.c in Sources */ = {isa = PBXBuildFile; fileRef = 28D7205B2380C2090038D439 /* resolver.c */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -65,6 +68,12 @@ 282126B3235C0EBF00072D52 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/LaunchScreen.strings"; sourceTree = ""; }; 282126B5235C0F5400072D52 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 282126B8235C0F8400072D52 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = ""; }; + 28D7204E238067B20038D439 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; + 28D72050238067EC0038D439 /* libresolv.9.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.9.tbd; path = usr/lib/libresolv.9.tbd; sourceTree = SDKROOT; }; + 28D720522380689E0038D439 /* Pi-Helper-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Pi-Helper-Bridging-Header.h"; sourceTree = ""; }; + 28D7205823808E8C0038D439 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + 28D7205A2380C2090038D439 /* resolver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = resolver.h; sourceTree = ""; }; + 28D7205B2380C2090038D439 /* resolver.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = resolver.c; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -72,6 +81,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 28D72051238067F30038D439 /* libresolv.9.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -99,6 +109,7 @@ 28212685235926EB00072D52 /* Pi-HelperTests */, 28212690235926EB00072D52 /* Pi-HelperUITests */, 2821266D235926E700072D52 /* Products */, + 28D7204D238067B20038D439 /* Frameworks */, ); sourceTree = ""; }; @@ -115,6 +126,7 @@ 2821266E235926E700072D52 /* Pi-Helper */ = { isa = PBXGroup; children = ( + 28D720522380689E0038D439 /* Pi-Helper-Bridging-Header.h */, 2821266F235926E700072D52 /* AppDelegate.swift */, 28212671235926E700072D52 /* SceneDelegate.swift */, 28212673235926E700072D52 /* ContentView.swift */, @@ -129,6 +141,9 @@ 282126AB235BF09800072D52 /* PiHoleDetailsView.swift */, 282126AF235BF52F00072D52 /* ActivityIndicatorView.swift */, 282126B6235C0F5400072D52 /* Localizable.strings */, + 28D7205823808E8C0038D439 /* Extensions.swift */, + 28D7205A2380C2090038D439 /* resolver.h */, + 28D7205B2380C2090038D439 /* resolver.c */, ); path = "Pi-Helper"; sourceTree = ""; @@ -159,6 +174,15 @@ path = "Pi-HelperUITests"; sourceTree = ""; }; + 28D7204D238067B20038D439 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 28D72050238067EC0038D439 /* libresolv.9.tbd */, + 28D7204E238067B20038D439 /* libresolv.tbd */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -227,6 +251,7 @@ TargetAttributes = { 2821266B235926E700072D52 = { CreatedOnToolsVersion = 11.0; + LastSwiftMigration = 1120; }; 28212681235926EB00072D52 = { CreatedOnToolsVersion = 11.0; @@ -297,9 +322,11 @@ 282126AA235BEE0F00072D52 /* PiHoleApiService.swift in Sources */, 282126AE235BF21D00072D52 /* AddPiHoleView.swift in Sources */, 28212670235926E700072D52 /* AppDelegate.swift in Sources */, + 28D7205C2380C2090038D439 /* resolver.c in Sources */, 28212672235926E700072D52 /* SceneDelegate.swift in Sources */, 282126B0235BF52F00072D52 /* ActivityIndicatorView.swift in Sources */, 28212674235926E700072D52 /* ContentView.swift in Sources */, + 28D7205923808E8C0038D439 /* Extensions.swift in Sources */, 282126A8235BE72800072D52 /* PiHole.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -477,6 +504,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"Pi-Helper/Preview Content\""; DEVELOPMENT_TEAM = 9Z6DE6KNJ9; @@ -488,6 +516,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "com.wbrawner.Pi-Helper"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Pi-Helper/Pi-Helper-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -497,6 +527,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"Pi-Helper/Preview Content\""; DEVELOPMENT_TEAM = 9Z6DE6KNJ9; @@ -508,6 +539,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "com.wbrawner.Pi-Helper"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Pi-Helper/Pi-Helper-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/Pi-Helper.xcodeproj/xcuserdata/billy.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Pi-Helper.xcodeproj/xcuserdata/billy.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist deleted file mode 100644 index 1b8e3e1..0000000 --- a/Pi-Helper.xcodeproj/xcuserdata/billy.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Pi-Helper/AddPiHoleView.swift b/Pi-Helper/AddPiHoleView.swift index 612892e..adfceae 100644 --- a/Pi-Helper/AddPiHoleView.swift +++ b/Pi-Helper/AddPiHoleView.swift @@ -9,17 +9,22 @@ import SwiftUI struct AddPiHoleView: View { - @State var ipAddress: String = "" - @State var apiKey: String = "" + var statefulContent: AnyView { + switch self.dataStore.pihole { + case .failure(let piholeError): + switch piholeError { + case .scanning(let ipAddress): + return ScanningView(dataStore: self.dataStore, ipAddress: ipAddress).toAnyView() + default: + return ManuallyAddPiHoleView(dataStore: self.dataStore).toAnyView() + } + default: + return ManuallyAddPiHoleView(dataStore: self.dataStore).toAnyView() + } + } var body: some View { - VStack { - Text("add_pihole") - TextField("ip_address", text: $ipAddress) - SecureField("api_key_optional", text: $apiKey) - Button(action: { self.dataStore.loadSummary(self.ipAddress, apiKey: self.apiKey) }, label: { Text("connect") }) - } - .padding() + statefulContent } @ObservedObject var dataStore: PiHoleDataStore @@ -28,6 +33,62 @@ struct AddPiHoleView: View { } } +struct ManuallyAddPiHoleView: View { + @State var ipAddress: String = "" + @State var apiKey: String = "" + @ObservedObject var dataStore: PiHoleDataStore + + var body: some View { + VStack { + Text("add_pihole") + TextField("ip_address", text: $ipAddress) + SecureField("api_key_optional", text: $apiKey) + Button(action: { + self.dataStore.baseUrl = self.ipAddress + self.dataStore.apiKey = self.apiKey + self.dataStore.loadSummary() + }, label: { Text("connect") }) + .padding() + ScanButton(dataStore: self.dataStore) + } + .padding() + } +} + +struct ScanButton: View { + @ObservedObject var dataStore: PiHoleDataStore + + var statefulContent: AnyView { + if let deviceIpAddress = resolver_get_device_ip() { + return Button(action: { + self.dataStore.beginScanning(String(cString: deviceIpAddress)) + }, label: { Text("scan") }).padding().toAnyView() + } else { + return EmptyView().toAnyView() + } + } + + var body: some View { statefulContent } +} + +struct ScanningView: View { + let dataStore: PiHoleDataStore + let ipAddress: String + + var body: some View { + VStack { + ActivityIndicatorView(.constant(true)) + Text("scanning_ip_address") + Text(verbatim: ipAddress) + Button(action: { + self.dataStore.cancelRequest() + }, label: { Text("cancel") } + ) + .padding() + } + } +} + struct AddPiHoleView_Previews: PreviewProvider { static var previews: some View { AddPiHoleView(PiHoleDataStore()) diff --git a/Pi-Helper/ContentView.swift b/Pi-Helper/ContentView.swift index 6209c2a..73fd578 100644 --- a/Pi-Helper/ContentView.swift +++ b/Pi-Helper/ContentView.swift @@ -16,11 +16,11 @@ struct ContentView: View { var stateContent: AnyView { switch self.dataStore.pihole { case .success(_): - return AnyView(PiHoleDetailsView(self.dataStore)) - case .failure(.loading): - return AnyView(ActivityIndicatorView(.constant(true))) + return PiHoleDetailsView(self.dataStore).toAnyView() + case .failure(.networkError(.loading)): + return ActivityIndicatorView(.constant(true)).toAnyView() default: - return AnyView(AddPiHoleView(self.dataStore)) + return AddPiHoleView(self.dataStore).toAnyView() } } diff --git a/Pi-Helper/Extensions.swift b/Pi-Helper/Extensions.swift new file mode 100644 index 0000000..f2571a4 --- /dev/null +++ b/Pi-Helper/Extensions.swift @@ -0,0 +1,16 @@ +// +// Extensions.swift +// Pi-Helper +// +// Created by Billy Brawner on 11/16/19. +// Copyright © 2019 William Brawner. All rights reserved. +// + +import Foundation +import SwiftUI + +extension View { + func toAnyView() -> AnyView { + return AnyView(self) + } +} diff --git a/Pi-Helper/Pi-Helper-Bridging-Header.h b/Pi-Helper/Pi-Helper-Bridging-Header.h new file mode 100644 index 0000000..cf340f6 --- /dev/null +++ b/Pi-Helper/Pi-Helper-Bridging-Header.h @@ -0,0 +1,5 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#include "resolver.h" diff --git a/Pi-Helper/PiHole.swift b/Pi-Helper/PiHole.swift index df08990..8880589 100644 --- a/Pi-Helper/PiHole.swift +++ b/Pi-Helper/PiHole.swift @@ -110,6 +110,10 @@ struct StatusUpdate: Codable { let status: String } +struct VersionResponse: Codable { + let version: Int +} + enum PiHoleStatus { case enabled case disabled diff --git a/Pi-Helper/PiHoleApiService.swift b/Pi-Helper/PiHoleApiService.swift index d2b2c59..5822b25 100644 --- a/Pi-Helper/PiHoleApiService.swift +++ b/Pi-Helper/PiHoleApiService.swift @@ -20,6 +20,10 @@ class PiHoleApiService { self.decoder = decoder } + func getVersion() -> AnyPublisher { + return get(queries: ["version": nil]) + } + func loadSummary() -> AnyPublisher { return get() } @@ -68,6 +72,7 @@ class PiHoleApiService { var request = URLRequest(url: url) request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.httpMethod = "GET" + request.timeoutInterval = 0.5 let task = URLSession.shared.dataTaskPublisher(for: request) .tryMap { (data, res) -> Data in @@ -91,6 +96,7 @@ class PiHoleApiService { enum NetworkError: Error { case loading + case cancelled case badRequest case notFound case unauthorized diff --git a/Pi-Helper/PiHoleDataStore.swift b/Pi-Helper/PiHoleDataStore.swift index ceef5f9..57b21a6 100644 --- a/Pi-Helper/PiHoleDataStore.swift +++ b/Pi-Helper/PiHoleDataStore.swift @@ -11,22 +11,95 @@ import Combine import UIKit class PiHoleDataStore: ObservableObject { - var pihole: Result = .failure(.notFound) { + private let IP_MIN = 0 + private let IP_MAX = 255 + private var currentRequest: AnyCancellable? = nil + @Published var pihole: Result = .failure(.notConfigured) + var apiKey: String? = nil { didSet { - self.objectWillChange.send() + UserDefaults.standard.set(apiKey, forKey: PiHoleDataStore.API_KEY) + apiService.apiKey = apiKey + } + } + var baseUrl: String? = nil { + didSet { + let safeHost = prependScheme(baseUrl) + UserDefaults.standard.set(safeHost, forKey: PiHoleDataStore.HOST_KEY) + apiService.baseUrl = safeHost } } - func loadSummary(_ host: String, apiKey: String? = nil) { - self.pihole = .failure(.loading) - - var safeHost = host - if !host.starts(with: "http://") || !host.starts(with: "https://") { - safeHost = "http://" + safeHost + private func prependScheme(_ ipAddress: String?) -> String? { + guard let host = ipAddress else { + return nil } - apiService.baseUrl = safeHost - apiService.apiKey = apiKey - _ = apiService.loadSummary() + + if !host.starts(with: "http://") && !host.starts(with: "https://") { + return "http://" + host + } + + return host + } + + func beginScanning(_ ipAddress: String) { + var addressParts = ipAddress.split(separator: ".") + var chunks = 1 + var ipAddresses = [String]() + while chunks <= IP_MAX { + let chunkSize = (IP_MAX - IP_MIN + 1) / chunks + if chunkSize == 1 { + return + } + for chunk in 0..ifa_next, n++) { + if (ifa->ifa_addr == NULL) { + continue; + } + + if (ifa->ifa_addr->sa_family != AF_INET) { + continue; + } + + if (strcmp("en0", ifa->ifa_name) == 0 + || strcmp("en1", ifa->ifa_name) == 0 + || strcmp("en2", ifa->ifa_name) == 0 + || strcmp("en3", ifa->ifa_name) == 0 + || strcmp("en4", ifa->ifa_name) == 0) { + char *ipAddress = malloc(NI_MAXHOST + 1); + ipAddress[NI_NUMERICHOST] = '\0'; + getnameinfo( + ifa->ifa_addr, + ifa->ifa_addr->sa_len, + ipAddress, + NI_MAXHOST, + NULL, + 0, + NI_NUMERICHOST + ); + freeifaddrs(ifaddr); + return ipAddress; + } + } + + freeifaddrs(ifaddr); + return NULL; +} diff --git a/Pi-Helper/resolver.h b/Pi-Helper/resolver.h new file mode 100644 index 0000000..b6657ed --- /dev/null +++ b/Pi-Helper/resolver.h @@ -0,0 +1,26 @@ +// +// resolver.h +// Pi-Helper +// +// Created by Billy Brawner on 11/16/19. +// Copyright © 2019 William Brawner. All rights reserved. +// + +#ifndef resolver_h +#define resolver_h + +#include +#include +#include +#include +#include +#include + +const int MAX_SERVERS = 10; +// String length for max IP address length (255.255.255.255) +const int MAX_IP_ADDR_LEN = 15; + +char * resolver_get_dns_server_ip(void); +char * resolver_get_device_ip(void); + +#endif /* resolver_h */