Skip to content
This repository has been archived by the owner on Sep 8, 2019. It is now read-only.

Commit

Permalink
Merge pull request #2 from agottardo/v2
Browse files Browse the repository at this point in the history
Add custom IP, XCTests with expectations, more documentation.
  • Loading branch information
agottardo authored Dec 23, 2018
2 parents b4b5366 + 626c21e commit 83062ba
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 66 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
language: objective-c
osx_image: xcode10
script: xcodebuild clean test -project IPKit.xcodeproj -scheme IPKit -destination "platform=iOS Simulator,name=iPhone X,OS=12.0" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO -quiet
6 changes: 6 additions & 0 deletions IPKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
40D9A61A21C98FBF00A6AB27 /* Singleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40D9A61821C98FBD00A6AB27 /* Singleton.swift */; };
40D9A61D21C98FFB00A6AB27 /* Structs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40D9A61C21C98FFB00A6AB27 /* Structs.swift */; };
40D9A61E21C9900000A6AB27 /* Structs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40D9A61C21C98FFB00A6AB27 /* Structs.swift */; };
40FB1C1121D0412500E0442B /* .travis.yml in Resources */ = {isa = PBXBuildFile; fileRef = 40FB1C1021D0412500E0442B /* .travis.yml */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand All @@ -36,6 +37,7 @@
40D9A60E21C98FA100A6AB27 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
40D9A61821C98FBD00A6AB27 /* Singleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Singleton.swift; sourceTree = "<group>"; };
40D9A61C21C98FFB00A6AB27 /* Structs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Structs.swift; sourceTree = "<group>"; };
40FB1C1021D0412500E0442B /* .travis.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .travis.yml; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -60,6 +62,7 @@
40D9A5F421C98FA100A6AB27 = {
isa = PBXGroup;
children = (
40FB1C1021D0412500E0442B /* .travis.yml */,
4099A3BD21C9B6A2009FB14D /* README.md */,
40D9A60021C98FA100A6AB27 /* IPKit */,
40D9A60B21C98FA100A6AB27 /* IPKitTests */,
Expand Down Expand Up @@ -196,6 +199,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
40FB1C1121D0412500E0442B /* .travis.yml in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -374,6 +378,7 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = IPKit/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -401,6 +406,7 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = IPKit/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down
100 changes: 100 additions & 0 deletions IPKit.xcodeproj/xcshareddata/xcschemes/IPKit.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1010"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "40D9A5FD21C98FA100A6AB27"
BuildableName = "IPKit.framework"
BlueprintName = "IPKit"
ReferencedContainer = "container:IPKit.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
codeCoverageEnabled = "YES"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "40D9A60621C98FA100A6AB27"
BuildableName = "IPKitTests.xctest"
BlueprintName = "IPKitTests"
ReferencedContainer = "container:IPKit.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "40D9A5FD21C98FA100A6AB27"
BuildableName = "IPKit.framework"
BlueprintName = "IPKit"
ReferencedContainer = "container:IPKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "40D9A5FD21C98FA100A6AB27"
BuildableName = "IPKit.framework"
BlueprintName = "IPKit"
ReferencedContainer = "container:IPKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "40D9A5FD21C98FA100A6AB27"
BuildableName = "IPKit.framework"
BlueprintName = "IPKit"
ReferencedContainer = "container:IPKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
type = "1"
version = "2.0">
</Bucket>
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,18 @@
<integer>0</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>40D9A5FD21C98FA100A6AB27</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>40D9A60621C98FA100A6AB27</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
</plist>
11 changes: 10 additions & 1 deletion IPKit/Model/Structs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,23 @@ struct IPLocation {
var continentCode : String?
var postalCode : String?
var coordinates : CLLocationCoordinate2D?
var timeZone : TimeZone?
var countryCallingCode : String?
var currency : String?
var languages : [String]?
var isInEuropeanUnion : Bool?
}

/**
Represents the Autonomous System information associated
with an IP address.
*/
struct IPASNumber {
var asNumber : String?
var organizationName : String?
}

/// An error returned by the API.
enum IPKitError : Error {
/// Thrown when `fetch` is called with an invalid name.
case InvalidNameProvided
}
138 changes: 101 additions & 37 deletions IPKit/Singleton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,55 @@
import Foundation
import CoreLocation

/**
Entry point to the API. This must be used as a singleton, so
make sure you always call `IPAPI.sharedInstance` to access this class.
*/
class IPAPI {

private init() {}
/// Singleton instance...
static let shared = IPAPI()

let endpoint = URL(string: "https://ipapi.co/json")!
/// ... so we provide a private initializer.
private init() {}

/// Timeout for network requests (default = 10 seconds).
var timeout : TimeInterval = 10

/**
Sets a timeout for the network request to the API.
*/
func setTimeout(seconds: TimeInterval) {
timeout = seconds
}

func fetch(completion: @escaping (IPResponse?, Error?) -> ()) {
/**
Calls the API and obtains information regarding the current IP address.
- Parameter completion: completion handler.
*/
func fetch(completion: @escaping (_ response: IPResponse?, _ error: Error?) -> ()) {
let endpoint = URL(string: "https://ipapi.co/json")!
fetchFromAPI(requestURL: endpoint, completion: completion)
}

/**
Calls the API and obtains information regarding the given IP address.
- Parameter forIP: address to lookup.
- Parameter completion: completion handler.
*/
func fetch(forIP: String, completion: @escaping (_ response: IPResponse?, _ error: Error?) -> ()) {
if !verifyHostname(hostname: forIP) {
completion(nil, IPKitError.InvalidNameProvided)
return
}
let endpoint = URL(string: "https://ipapi.co/" + forIP + "/json")!
fetchFromAPI(requestURL: endpoint, completion: completion)
}

// - MARK: Network calls

private func fetchFromAPI(requestURL: URL, completion: @escaping (IPResponse?, Error?) -> ()) {
let session = URLSession(configuration: .default)
var request = URLRequest(url: endpoint, cachePolicy: URLRequest.CachePolicy.reloadRevalidatingCacheData, timeoutInterval: timeout)
var request = URLRequest(url: requestURL, cachePolicy: URLRequest.CachePolicy.reloadRevalidatingCacheData, timeoutInterval: timeout)
request.httpMethod = "GET"
request.httpShouldHandleCookies = false
let task = session.dataTask(with: request) { (data, response, error) in
Expand All @@ -32,43 +66,73 @@ class IPAPI {
return
}
do {
let obj = try JSONSerialization.jsonObject(with: data!, options: []) as? [String:Any]
var ipResponse = IPResponse()
ipResponse.ip = obj?["ip"] as? String
var location = IPLocation()
var languages = [String]()
(obj?["languages"] as? String)?.split(separator: ",").forEach({ (s) in
languages.append(String(s))
})
location.languages = languages
location.country = obj?["country"] as? String
location.countryName = obj?["country_name"] as? String
location.city = obj?["city"] as? String
location.currency = obj?["currency"] as? String
location.continentCode = obj?["continent_code"] as? String
location.countryCallingCode = obj?["country_calling_code"] as? String
location.region = obj?["region"] as? String
location.regionCode = obj?["region_code"] as? String
location.postalCode = obj?["postal"] as? String
location.isInEuropeanUnion = obj?["in_eu"] as? Bool
if let lat = obj?["latitude"] as? Double, let lon = obj?["longitude"] as? Double {
let latLon = CLLocationCoordinate2D(latitude: lat, longitude: lon)
location.coordinates = latLon
}
var asn = IPASNumber()
asn.asNumber = obj?["asn"] as? String
asn.organizationName = obj?["org"] as? String
if asn.asNumber != nil {
ipResponse.asn = asn
}
ipResponse.location = location
completion(ipResponse, error)
let response = try self.parseResponse(responseData: data!)
completion(response, nil)
} catch {
completion(nil, error)
}

}
task.priority = 1.0
task.resume()
}

private func parseResponse(responseData: Data) throws -> IPResponse {
do {
let obj = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String:Any]
var ipResponse = IPResponse()
ipResponse.ip = obj?["ip"] as? String
var location = IPLocation()
var languages = [String]()
(obj?["languages"] as? String)?.split(separator: ",").forEach({ (s) in
languages.append(String(s))
})
location.languages = languages
location.country = obj?["country"] as? String
location.countryName = obj?["country_name"] as? String
location.city = obj?["city"] as? String
location.currency = obj?["currency"] as? String
location.continentCode = obj?["continent_code"] as? String
location.countryCallingCode = obj?["country_calling_code"] as? String
location.region = obj?["region"] as? String
location.regionCode = obj?["region_code"] as? String
location.postalCode = obj?["postal"] as? String
location.isInEuropeanUnion = obj?["in_eu"] as? Bool
if let lat = obj?["latitude"] as? Double, let lon = obj?["longitude"] as? Double {
let latLon = CLLocationCoordinate2D(latitude: lat, longitude: lon)
location.coordinates = latLon
}
var asn = IPASNumber()
asn.asNumber = obj?["asn"] as? String
asn.organizationName = obj?["org"] as? String
if asn.asNumber != nil {
ipResponse.asn = asn
}
ipResponse.location = location
return ipResponse
} catch {
throw error
}
}

// - MARK: Error handling

/**
Returns whether the entered hostname is a valid IP address
or domain name.
*/
private func verifyHostname(hostname: String) -> Bool {

let ipRegex = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
let hostnameRegex = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$"

if (hostname.range(of: ipRegex, options: .regularExpression) != nil) {
return true
}
if (hostname.range(of: hostnameRegex, options: .regularExpression) != nil) {
return true
}
return false

}

}
Loading

0 comments on commit 83062ba

Please sign in to comment.