Merge pull request #1 from rinold/develop

Merging for 0.1.0 release
This commit is contained in:
Mikhail Churbanov 2018-06-01 00:09:55 +03:00 committed by GitHub
commit 0f12b8cc6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 293 additions and 89 deletions

1
.swift-version Normal file
View File

@ -0,0 +1 @@
4.1

View File

@ -1,14 +1,6 @@
# references:
# * https://www.objc.io/issues/6-build-tools/travis-ci/
# * https://github.com/supermarin/xcpretty#usage
language: swift
osx_image: xcode9
osx_image: xcode7.3
language: objective-c
# cache: cocoapods
# podfile: Example/Podfile
# before_install:
# - gem install cocoapods # Since Travis is not always on latest version
# - pod install --project-directory=Example
script:
- set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/ProxyResolver.xcworkspace -scheme ProxyResolver-Example -sdk iphonesimulator9.3 ONLY_ACTIVE_ARCH=NO | xcpretty
- set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/ProxyResolver.xcworkspace -scheme ProxyResolver-Example -sdk macosx test | xcpretty
- pod lib lint

View File

@ -119,7 +119,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0900;
LastUpgradeCheck = 0900;
LastUpgradeCheck = 0940;
ORGANIZATIONNAME = CocoaPods;
TargetAttributes = {
ED83F68920348A760038D96B = {
@ -222,6 +222,7 @@
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
@ -229,6 +230,7 @@
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
@ -256,7 +258,6 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.13;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
@ -279,6 +280,7 @@
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
@ -286,6 +288,7 @@
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
@ -307,7 +310,6 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.13;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0900"
LastUpgradeVersion = "0940"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,35 +1,160 @@
import XCTest
import ProxyResolver
@testable import ProxyResolver
class MockProxyConfigProvider: ProxyConfigProvider {
var testConfig: [[CFString : AnyObject]]?
func setTestConfig(_ config: [[CFString : AnyObject]]?) {
self.testConfig = config
}
func getSystemConfigProxies(for url: URL) -> [[CFString : AnyObject]]? {
return testConfig
}
}
enum TestConfigs {
enum noProxy {
static let config = [[kCFProxyTypeKey: kCFProxyTypeNone]]
}
enum http {
static let host = "http.proxy.com"
static let port = 8080
static let config = [
[kCFProxyTypeKey: kCFProxyTypeHTTP,
kCFProxyHostNameKey: host as AnyObject,
kCFProxyPortNumberKey: port as AnyObject]
]
}
enum https {
static let host = "http.proxy.com"
static let port = 8081
static let config = [
[kCFProxyTypeKey: kCFProxyTypeHTTPS,
kCFProxyHostNameKey: host as AnyObject,
kCFProxyPortNumberKey: port as AnyObject]
]
}
enum socks {
static let host = "socks.proxy.com"
static let port = 8082
static let config = [
[kCFProxyTypeKey: kCFProxyTypeSOCKS,
kCFProxyHostNameKey: host as AnyObject,
kCFProxyPortNumberKey: port as AnyObject]
]
}
}
class Tests: XCTestCase {
let testUrl = URL(string: "http://google.com")!
var testConfigProvider: MockProxyConfigProvider!
var proxy: ProxyResolver!
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
testConfigProvider = MockProxyConfigProvider()
proxy = ProxyResolver(configProvider: testConfigProvider)
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testExample() {
// This is an example of a functional test case.
func testNoProxyResolve() {
let expectation = XCTestExpectation(description: "Completion called")
let proxy = ProxyResolver()
let url = URL(string: "http://google.com")!
proxy.resolve(for: url) { result in
testConfigProvider.setTestConfig(TestConfigs.noProxy.config)
proxy.resolve(for: testUrl) { result in
switch result {
case .success(let proxy):
XCTAssertNil(proxy)
case .failure(let error):
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 10.0)
}
// func testAutoConfigurationUrlResolve() {
// XCTAssert(false)
// }
//
// func testAutoConfigurationScriptResolve() {
// XCTAssert(false)
// }
func testHttpResolve() {
let expectation = XCTestExpectation(description: "Completion called")
testConfigProvider.setTestConfig(TestConfigs.http.config)
proxy.resolve(for: testUrl) { result in
switch result {
case .success(let proxy):
XCTAssertNotNil(proxy)
XCTAssert(.http == proxy!.type)
XCTAssert(TestConfigs.http.host == proxy!.host)
XCTAssert(TestConfigs.http.port == proxy!.port)
case .failure(let error):
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 10.0)
}
func testHttpsResolve() {
let expectation = XCTestExpectation(description: "Completion called")
testConfigProvider.setTestConfig(TestConfigs.https.config)
proxy.resolve(for: testUrl) { result in
switch result {
case .success(let proxy):
XCTAssertNotNil(proxy)
XCTAssert(.https == proxy!.type)
XCTAssert(TestConfigs.https.host == proxy!.host)
XCTAssert(TestConfigs.https.port == proxy!.port)
case .failure(let error):
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 10.0)
}
func testSocksResolve() {
let expectation = XCTestExpectation(description: "Completion called")
testConfigProvider.setTestConfig(TestConfigs.socks.config)
proxy.resolve(for: testUrl) { result in
switch result {
case .success(let proxy):
XCTAssertNotNil(proxy)
XCTAssert(.socks == proxy!.type)
XCTAssert(TestConfigs.socks.host == proxy!.host)
XCTAssert(TestConfigs.socks.port == proxy!.port)
case .failure(let error):
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 10.0)
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure() {
// Put the code you want to measure the time of here.
}
}
}

View File

@ -9,7 +9,7 @@
Pod::Spec.new do |s|
s.name = 'ProxyResolver'
s.version = '0.1.0'
s.summary = 'A short description of ProxyResolver.'
s.summary = 'Simple resolution of user proxy settings for macOS'
# This description is used to generate tags and improve search results.
# * Think: What does it do? Why did you write it? What is the focus?
@ -18,7 +18,9 @@ Pod::Spec.new do |s|
# * Finally, don't worry about the indent, CocoaPods strips it!
s.description = <<-DESC
TODO: Add long description of the pod here.
ProxyResolver allows simply resolve the actual proxy information from users
system configuration and could be used for setting up Stream-based connections,
for example for Web Sockets.
DESC
s.homepage = 'https://github.com/rinold/ProxyResolver'

View File

@ -67,9 +67,45 @@ public enum ProxyResolutionError: Error {
public typealias ProxyResolutionCompletion = (ResolutionResult<Proxy>) -> Void
public typealias ProxiesResolutionCompletion = (ResolutionResult<[Proxy]>) -> Void
// MARK: - ProxyConfigProvide protocol
protocol ProxyConfigProvider {
func getSystemConfigProxies(for url: URL) -> [[CFString: AnyObject]]?
}
class SystemProxyConfigProvider: ProxyConfigProvider {
func getSystemConfigProxies(for url: URL) -> [[CFString: AnyObject]]? {
guard let systemSettings = CFNetworkCopySystemProxySettings()?.takeRetainedValue() else { return nil }
let cfProxies = CFNetworkCopyProxiesForURL(url as CFURL, systemSettings).takeRetainedValue()
return cfProxies as? [[CFString: AnyObject]]
}
}
// MARK: - Network abstraction protocol
protocol AutoConfigUrlFether {
func fetch(request: URLRequest, completion: @escaping (String?, Error?) -> Void)
}
class URLSessionFetcher: AutoConfigUrlFether {
func fetch(request: URLRequest, completion: @escaping (String?, Error?) -> Void) {
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if error == nil, let data = data, let scriptContents = String(data: data, encoding: .utf8) {
completion(scriptContents, nil)
} else {
completion(nil, error)
}
}
task.resume()
}
}
// MARK: - ProxyResolver class
public class ProxyResolver {
public final class ProxyResolver {
private var configProvider: ProxyConfigProvider
private var urlFetcher: AutoConfigUrlFether
private let supportedAutoConfigUrlShemes = ["http", "https"]
private let shemeNormalizationRules = ["ws": "http",
@ -77,11 +113,15 @@ public class ProxyResolver {
// MARK: Public Methods
public init() {} // TODO: Configuration for properties above
// TODO: Configuration for properties above
public init() {
configProvider = SystemProxyConfigProvider()
urlFetcher = URLSessionFetcher()
}
public func resolve(for url: URL, completion: @escaping ProxyResolutionCompletion) {
guard let normalizedUrl = urlWithNormalizedSheme(from: url) else { return }
guard let proxiesConfig = getSystemConfigProxies(for: normalizedUrl),
guard let proxiesConfig = configProvider.getSystemConfigProxies(for: normalizedUrl),
let firstProxyConfig = proxiesConfig.first
else {
let error = ProxyResolutionError.unexpectedError(nil)
@ -111,29 +151,20 @@ public class ProxyResolver {
resolveProxy(from: firstProxyConfig, for: normalizedUrl, completion: tryNextOnErrorCompletion)
}
public func resolveAll(for url: URL, completion: @escaping ProxiesResolutionCompletion) {
guard let normalizedUrl = urlWithNormalizedSheme(from: url) else { return }
guard let proxies = getSystemConfigProxies(for: normalizedUrl) else { return }
// resolveProxies(from: proxies, for: url, completion: completion)
}
// MARK: Internal Methods
// MARK: Private Methods
private func getSystemConfigProxies(for url: URL) -> [[CFString: AnyObject]]? {
guard let systemSettings = CFNetworkCopySystemProxySettings()?.takeRetainedValue() else { return nil }
let cfProxies = CFNetworkCopyProxiesForURL(url as CFURL, systemSettings).takeRetainedValue()
return cfProxies as? [[CFString: AnyObject]]
}
private func resolveProxies(from proxies: [CFDictionary], for url: URL, completion: @escaping ProxyResolutionCompletion) {
for proxyConfig in proxies.compactMap({ $0 as? [CFString: AnyObject] }) {
print (proxyConfig)
// getProxy(from: proxyConfig, for: url)
convenience init(configProvider: ProxyConfigProvider? = nil, urlFetcher: AutoConfigUrlFether? = nil) {
self.init()
if let configProvider = configProvider {
self.configProvider = configProvider
}
if let urlFetcher = urlFetcher {
self.urlFetcher = urlFetcher
}
}
private func resolveProxy(from config: [CFString: AnyObject], for url: URL,
completion: @escaping ProxyResolutionCompletion) {
func resolveProxy(from config: [CFString: AnyObject], for url: URL,
completion: @escaping ProxyResolutionCompletion) {
guard let proxyTypeValue = config[kCFProxyTypeKey] else {
let error = ProxyResolutionError.unexpectedError(nil)
@ -150,23 +181,51 @@ public class ProxyResolver {
switch proxyType {
case .autoConfigurationUrl:
guard let cfAutoConfigUrl = config[kCFProxyAutoConfigurationURLKey],
let urlString = cfAutoConfigUrl as? String,
let scriptUrl = URL(string: urlString)
let scriptUrlString = cfAutoConfigUrl as? String,
let scriptUrl = URL(string: scriptUrlString)
else {
// TODO: proper error handling
let error = ProxyResolutionError.unexpectedError(nil)
completion(.failure(error))
return
}
fetchPacScript(from: scriptUrl) { scriptContent, error in
guard let scriptContent = scriptContent else { return }
self.executePac(script: scriptContent, for: url)
guard let scriptContent = scriptContent else {
// TODO: proper error handling
let error = ProxyResolutionError.unexpectedError(nil)
completion(.failure(error))
return
}
// TODO: process all, first is for now
guard let pacProxyConfig = self.executePac(script: scriptContent, for: url)?.first else {
// TODO: proper error handling
let error = ProxyResolutionError.unexpectedError(nil)
completion(.failure(error))
return
}
// TODO: check if recursion here is possible?
self.resolveProxy(from: pacProxyConfig, for: url, completion: completion)
}
case .autoConfigurationScript:
guard let cfAutoConfigScript = config[kCFProxyAutoConfigurationJavaScriptKey],
let scriptContent = cfAutoConfigScript as? String
else {
// TODO: proper error handling
let error = ProxyResolutionError.unexpectedError(nil)
completion(.failure(error))
return
}
executePac(script: scriptContent, for: url)
// TODO: process all, first is for now
// TODO: check if code for pac should be moved out (same used after pac fetch above)
guard let pacProxyConfig = self.executePac(script: scriptContent, for: url)?.first else {
// TODO: proper error handling
let error = ProxyResolutionError.unexpectedError(nil)
completion(.failure(error))
return
}
// TODO: check if recursion here is possible?
self.resolveProxy(from: pacProxyConfig, for: url, completion: completion)
case .http, .https, .socks:
guard let cfProxyHost = config[kCFProxyHostNameKey],
@ -186,54 +245,36 @@ public class ProxyResolver {
}
}
private func fetchPacScript(from url: URL, completion: @escaping (String?, Error?) -> Void) {
func fetchPacScript(from url: URL, completion: @escaping (String?, Error?) -> Void) {
if url.isFileURL, let scriptContents = try? String(contentsOfFile: url.absoluteString) {
completion(scriptContents, nil)
return
}
guard let scheme = url.scheme?.lowercased(),
supportedAutoConfigUrlShemes.contains(scheme)
else {
guard let scheme = url.scheme?.lowercased(), supportedAutoConfigUrlShemes.contains(scheme) else {
completion(nil, nil)
return
}
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if error == nil, let data = data, let scriptContents = String(data: data, encoding: .utf8) {
completion(scriptContents, nil)
} else {
completion(nil, error)
}
}
task.resume()
urlFetcher.fetch(request: request, completion: completion)
}
private func executePac(script: String, for url: URL) {
// From: http://developer.apple.com/samplecode/CFProxySupportTool/listing1.html
// Work around <rdar://problem/5530166>. This dummy call to
// CFNetworkCopyProxiesForURL initialise some state within CFNetwork
// that is required by CFNetworkCopyProxiesForAutoConfigurationScript.
_ = CFNetworkCopyProxiesForURL(url as CFURL, NSDictionary()).takeRetainedValue()
func executePac(script: String, for url: URL) -> [[CFString: AnyObject]]? {
var error: Unmanaged<CFError>?
let proxiesCopy = CFNetworkCopyProxiesForAutoConfigurationScript(script as CFString, url as CFURL, &error)
guard error == nil,
let cfProxies = proxiesCopy?.takeRetainedValue(),
let proxies = cfProxies as? [CFDictionary]
let proxies = cfProxies as? [[CFString: AnyObject]]
else {
return
}
resolveProxies(from: proxies, for: url) { proxies in
return nil
}
return proxies
}
// MARK: - Private Helper Methods
// MARK: - Helper Methods
private func urlWithNormalizedSheme(from url: URL) -> URL? {
func urlWithNormalizedSheme(from url: URL) -> URL? {
guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true),
let scheme = urlComponents.scheme
else {
@ -243,7 +284,7 @@ public class ProxyResolver {
return urlComponents.url
}
fileprivate class func getCredentials(for host: String) -> Credentials? {
class func getCredentials(for host: String) -> Credentials? {
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrServer as String: host,
kSecMatchLimit as String: kSecMatchLimitOne,

View File

@ -5,11 +5,50 @@
[![License](https://img.shields.io/cocoapods/l/ProxyResolver.svg?style=flat)](https://cocoapods.org/pods/ProxyResolver)
[![Platform](https://img.shields.io/cocoapods/p/ProxyResolver.svg?style=flat)](https://cocoapods.org/pods/ProxyResolver)
## Example
ProxyResolver allows simply resolve the actual proxy information from users
system configuration and could be used for setting up Stream-based connections,
for example for Web Sockets.
To run the example project, clone the repo, and run `pod install` from the Example directory first.
Usage example:
```swift
import ProxyResolver
let proxy = ProxyResolver()
let url = URL(string: "https://github.com")!
proxy.resolve(for: url) { result in
switch result {
case .success(let proxy):
guard let proxy = proxy else {
// no proxy required
}
// here you can establish connection to proxy or whatever you want
// print ("For \(url) use \(proxy.host):\(proxy.port)")
case .failure(let error):
// Handle error
}
}
```
## Features
#### Supported system configurations
- [x] Auto Proxy Discovery*
- [x] Automatic Proxy Configuration URL*
- [x] Web Proxy
- [x] Socks
> \* due to ATS protection auto-configuration url should be HTTPS or have \*.local or unresolvable globally domain with `NSAllowsLocalNetworking` key configured in plist. More info could be found in [NSAppTransportSecurity reference](https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW33).
#### Other (TBD)
- Proxy with required password support
> `Proxy.credentials` will automatically access `Proxy` keychain to retrieve configured for proxy account and password. As it would require permission from user the credentials are retrieved lazily only when you try to get them.
## Requirements
- Swift: 4+
- macOS: 10.10+
## Installation
@ -22,6 +61,8 @@ pod 'ProxyResolver'
## Author
ProxyResolver was initially inspired by the Starscream proxy support merge request.
rinold, mihail.churbanov@gmail.com
## License