This repository has been archived by the owner on Sep 24, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Auth.swift
785 lines (691 loc) · 36.2 KB
/
Auth.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
//
// Auth.swift
// Tunnels
//
// Copyright © 2018 Confirmed, Inc. All rights reserved.
//
import Alamofire
import CocoaLumberjackSwift
import PromiseKit
//MARK: - COOKIE RETRIER
//should we re-do using promises?
class CookieHandler : RequestRetrier {
static let cookieSemaphore = DispatchSemaphore(value: 1)
static var cookieAuthenticated = false //use this variable to prevent multiple requests of re-auth if expired. Is it necessary?
public func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == Global.kInvalidAuth {
DDLogWarn("401 response \(request.retryCount)")
//for 401 (invalid login), re-sign in
CookieHandler.cookieAuthenticated = false
if request.retryCount < 3 {
let delay = 0.1 //1.0 + pow(2.0, Double(request.retryCount)) //exponentially delay retry
CookieHandler.cookieSemaphore.wait(timeout: DispatchTime.now() + 4)
if CookieHandler.cookieAuthenticated {
CookieHandler.cookieSemaphore.signal()
completion(true, 0.1)
return
}
Auth.signInForCookie() { (status, code) in
CookieHandler.cookieSemaphore.signal()
if status {
completion(true, 0)
CookieHandler.cookieAuthenticated = true
}
else {
completion(true, delay)
}
}
}
else {
completion(false, 0.0)
}
} else {
completion(false, 0.0) // don't retry other codes yet
}
}
}
class Auth: NSObject {
public static var signInError = 0
static var cookieQueue = OperationQueue.init()
static let cookieSemaphore = DispatchSemaphore(value: 1)
//MARK: - COOKIE METHODS
/*
* switch API version
* primarily to cycle through deprecated API servers if primary one fails
* need to clear p12 & User ID as they are API specific
* when switching, always increases version of API & then reset (v1 -> v2 -> v3 -> v1)
*/
public static func incrementAPIVersion() {
if Global.isVersion(version: .v1API) {
UserDefaults.standard.set(APIVersionType.v2API, forKey: Global.kConfirmedAPIVersion)
UserDefaults.standard.synchronize()
}
else if Global.isVersion(version: APIVersionType.v2API) {
UserDefaults.standard.set(APIVersionType.v3API, forKey: Global.kConfirmedAPIVersion)
UserDefaults.standard.synchronize()
}
else {
UserDefaults.standard.set(APIVersionType.v1API, forKey: Global.kConfirmedAPIVersion)
UserDefaults.standard.synchronize()
}
NotificationCenter.post(name: .switchingAPIVersions)
//clear defaults in case of a version API
Global.keychain[Global.kConfirmedID] = nil
Global.keychain[Global.kConfirmedP12Key] = nil
let defaults = UserDefaults.standard
defaults.removeObject(forKey: Global.vpnSavedRegionKey)
defaults.set(Global.vpnDomain, forKey: Global.kLastEnvironment)
defaults.synchronize()
}
/*
* accept headers from /signin response
* return true if cookie headers are there
* return false if cookie headers are absent
* remove P12 & UserID in case there is an API version switch
*/
public static func processCookiesForHeader(response : DataResponse<Any>) -> Bool {
if let headerFields = response.response?.allHeaderFields as? [String: String],
let URL = response.request?.url
{
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headerFields, for: URL)
Alamofire.SessionManager.default.session.configuration.httpCookieStorage?.setCookies(cookies, for: URL, mainDocumentURL: nil)
//put in a method later
Global.keychain[Global.kConfirmedID] = nil
Global.keychain[Global.kConfirmedP12Key] = nil
let defaults = UserDefaults.standard
defaults.set(Global.vpnDomain, forKey: Global.kLastEnvironment)
defaults.synchronize()
return true
}
return false
}
public static func processPartnerCode() -> String? {
let pasteboard = UIPasteboard.general
if let partnerCodeData = pasteboard.data(forPasteboardType: Global.kPartnerCodePasteboardType), let partnerCode = String.init(data: partnerCodeData, encoding: .utf8)
{
var components = partnerCode.split(separator: "-")
if components.count == 3 && components[0] == Global.kConfirmedUniquePartnerCode { //check for Confirmed unique prefix
Global.sharedUserDefaults().set(partnerCode, forKey: Global.kPartnerCodePasteboardType)
Global.sharedUserDefaults().synchronize()
}
}
if let partnerCode = Global.sharedUserDefaults().value(forKey: Global.kPartnerCodePasteboardType) as? String {
var components = partnerCode.split(separator: "-")
if components.count == 3 && components[0] == Global.kConfirmedUniquePartnerCode { //check for Confirmed unique prefix
components.remove(at: 0)
let output = components.joined(separator: "-")
return output
}
}
return nil
}
/*
* /signin for cookie only returns 200 on success
* reject promise for any other, pass error up
* after successful cookie, test with get-key
* need to separate V1 as it does not return JSON on success
*/
public static func signInForCookieInternal(parameters : Dictionary<String, Any>) -> Promise<Bool> {
let sessionManager = Alamofire.SessionManager.default
sessionManager.retrier = nil
Auth.clearCookies()
var signinParams = parameters
if let partnerCode = processPartnerCode() {
signinParams["partner"] = partnerCode
}
return Promise { seal in
sessionManager.request(Global.signinURL, method: .post, parameters : signinParams, headers: headersForRequest()).responseJSON { response in
switch response.result {
case .success:
if response.response?.statusCode == 200 {
let serverResponse = processServerResponse(data: response.data)
if let eCode = serverResponse.code, eCode != 0 {
seal.reject(NSError.init(domain: "Sign In Error", code: eCode, userInfo:serverResponse.dictionary))
}
else if processCookiesForHeader(response: response) {
seal.fulfill(true) //credentials are valid without 401
}
else {
seal.reject(NSError.init(domain: "Sign In Error", code: Global.kInvalidAuth, userInfo: nil))
}
}
else {
let serverResponse = processServerResponse(data: response.data)
if let eCode = serverResponse.code, eCode != 0 {
seal.reject(NSError.init(domain: "Sign In Error", code: eCode, userInfo: serverResponse.dictionary))
}
else if let respCode = response.response?.statusCode {
seal.reject(NSError.init(domain: "Sign In Error", code: respCode, userInfo: nil))
}
else {
seal.reject(NSError.init(domain: "Sign In Error", code: Global.kUnknownError, userInfo: nil))
}
}
case .failure(let error):
if Global.isVersion(version: .v1API) && processCookiesForHeader(response: response) { //deprecated V1 does not return valid JSON, so catch false positive error case here
seal.fulfill(true)
return
}
if error is AFError, let statusCode = (error as! AFError).responseCode {
seal.reject(NSError.init(domain: "Sign In Error", code: statusCode, userInfo: nil))
}
else if error is NSError {
let statusCode = (error as NSError).code
seal.reject(NSError.init(domain: "Sign In Error", code: statusCode, userInfo: nil))
}
else {
seal.reject(NSError.init(domain: "Sign In Error", code: Global.kUnknownError, userInfo: nil))
}
}
}
}
}
static func extractP12Cert() {
if let userP12B64 = Global.keychain[Global.kConfirmedP12Key],
let p12Data = Data(base64Encoded: userP12B64)
{
let p12DataBytes = Int32(p12Data.count)
p12Data.withUnsafeBytes({ (bytes: UnsafePointer<Int8>) -> Void in
var caCert : UnsafeMutablePointer<UInt8>?
var caCertLen : UInt32 = 0
var clCert : UnsafeMutablePointer<UInt8>?
var clCertLen : UInt32 = 0
var privateKey : UnsafeMutablePointer<UInt8>?
var privateKeyLength : UInt32 = 0
processP12(bytes, p12DataBytes, &caCert, &caCertLen, &clCert, &clCertLen, &privateKey, &privateKeyLength)
let privateKeyString = String(cString: privateKey!)
let caCertString = String(cString: caCert!)
let clCertString = String(cString: clCert!)
Global.keychain[Global.kConfirmedPrivateKey] = privateKeyString.trimAfterPhrase(phrase: "-----END PRIVATE KEY-----")
Global.keychain[Global.kConfirmedCACertKey] = caCertString.trimAfterPhrase(phrase: "-----END CERTIFICATE-----")
Global.keychain[Global.kConfirmedCLCertKey] = clCertString.trimAfterPhrase(phrase: "-----END CERTIFICATE-----")
})
}
else {
//unable to extract, force IPSEC
Utils.setActiveProtocol(activeProtocol: IPSecV3.protocolName)
}
}
/*
* attempt /signin for supplied e-mail credentials (if inputted)
* this is only from sign in, so no need to try other authentication
* attempt /signin for saved email credentials (if available)
* if this is 401, we should clear credentials
* attempt /signin for receipt auth (if iOS & available)
* fail only if all methods fail
*/
public static func attemptAllAuthForVersion(email: String? = nil, password: String? = nil) -> Promise<Bool> {
if let userEmail = email, let userPassword = password {
return signInForCookieInternal(parameters: parametersForValues(email: userEmail, password: userPassword))
.then { promise -> Promise<Bool> in
//save parameters
Global.keychain[Global.kConfirmedEmail] = userEmail
Global.keychain[Global.kConfirmedPassword] = userPassword
return Promise.value(promise)
}
}
return firstly { () -> Promise<Bool> in //firstly try with supplied email/password
if let userEmail = Global.keychain[Global.kConfirmedEmail], let userPassword = Global.keychain[Global.kConfirmedPassword] {
DDLogInfo("Signing in with saved email: \(userEmail)")
return signInForCookieInternal(parameters: parametersForValues(email: userEmail, password: userPassword))
}
else {
throw NSError(domain: "Invalid credentials", code: 1, userInfo: nil)
}
}.recover { error -> Promise<Bool> in
#if os(iOS)
if let receipt = Global.keychain[Global.kConfirmedReceiptKey], receipt.count > 10 {
DDLogInfo("Signing in with receipt")
return signInForCookieInternal(parameters: parametersForValues(platform: Global.kPlatformiOS, authType: Global.kPlatformiOS, authReceipt: receipt))
}
else {
throw NSError(domain: "error", code: (error as NSError)._code, userInfo: nil)
}
#else
throw NSError(domain: "error", code: (error as NSError)._code, userInfo: nil)
#endif
}
}
/*
* parent cookie retrieval method
* check if cookie is available (from another thread) before requesting cookie
* request is serial in operation queue to not have users help DDoS our server
* attempt /signin for all possible credential methods
* if fails, switch to alternative version API (will deprecate V1 soon) and try again
* need to clear P12 & ID on API version switch
*/
public static func signInForCookie(email: String? = nil, password: String? = nil, forceReceiptAuth: Bool? = false, cookieCallback: @escaping (_ status: Bool, _ errorCode: Int) -> Void) {
cookieQueue.maxConcurrentOperationCount = 1
cookieQueue.addOperation {
DDLogInfo("Signing in for cookie")
Auth.cookieSemaphore.wait(timeout: DispatchTime.now() + 10) //rate limit sign in
if hasCookie() {
Auth.cookieSemaphore.signal()
cookieCallback(true, 0)
return
}
if email == nil || password == nil {
//reset to v3 version for receipt only as they are universal
UserDefaults.standard.set(APIVersionType.v3API, forKey: Global.kConfirmedAPIVersion)
UserDefaults.standard.synchronize()
}
//try active API version first
attemptAllAuthForVersion(email: email, password: password)
.recover { error -> Promise<Bool> in
let eCode = (error as NSError).code
if eCode == -1200 || eCode == -1001 || eCode == -1009 { //propagate bad Internet instead of switching API
return Promise.init(error: NSError.init(domain: (error as NSError).domain, code: Global.kInternetDownError, userInfo: nil))
}
if eCode == Global.kEmailNotConfirmed && !Global.isVersion(version: .v1API) { //don't switch API version on e-mail not confirmed if on V2 (V1 deprecated so no longer confirming accounts)
return Promise.init(error: NSError.init(domain: (error as NSError).domain, code: Global.kEmailNotConfirmed, userInfo: nil))
}
Auth.incrementAPIVersion()
return attemptAllAuthForVersion(email: email, password: password)
}
.recover { error -> Promise<Bool> in
let eCode = (error as NSError).code
if eCode == -1200 || eCode == -1001 || eCode == -1009 { //propagate bad Internet instead of switching API
return Promise.init(error: NSError.init(domain: (error as NSError).domain, code: Global.kInternetDownError, userInfo: nil))
}
if eCode == Global.kEmailNotConfirmed && !Global.isVersion(version: .v1API) { //don't switch API version on e-mail not confirmed if on V2 (V1 deprecated so no longer confirming accounts)
return Promise.init(error: NSError.init(domain: (error as NSError).domain, code: Global.kEmailNotConfirmed, userInfo: nil))
}
Auth.incrementAPIVersion()
return attemptAllAuthForVersion(email: email, password: password)
}
.done {_ in
Auth.cookieSemaphore.signal()
cookieCallback(true, 0)
}
.catch { error in //fallback to previous API version
let eCode = error as NSError
if error is AFError, let statusCode = (error as! AFError).responseCode, statusCode == Global.kInvalidAuth {
Global.keychain[Global.kConfirmedEmail] = nil
Global.keychain[Global.kConfirmedPassword] = nil
}
Auth.cookieSemaphore.signal()
cookieCallback(false, eCode.code) //invalid login
}
}
}
/*
* method to check for cookie in local storage
*/
public static func hasCookie() -> Bool {
var hasCookie = false
let cstorage = Alamofire.SessionManager.default.session.configuration.httpCookieStorage
if let cookies = cstorage?.cookies {
for cookie in cookies {
if let timeUntilExpire = cookie.expiresDate?.timeIntervalSinceNow {
if !Global.isVersion(version: .v3API) && UserDefaults.standard.bool(forKey: Global.kIsOnFinalDeprecatedV1V2) {
if cookie.domain.contains("confirmedvpn.co") && timeUntilExpire > 120.0 {
hasCookie = true
}
}
if cookie.domain.contains("confirmedvpn.com") && timeUntilExpire > 120.0 {
hasCookie = true
}
}
}
}
if !hasCookie {
Auth.clearCookies()
}
return hasCookie
}
/*
* method to check for cookie before every request
* if has cookie, continue call
* if no cookie, return cookie
* if retrieved cookie, continue call
* if no cookie, error out
*/
public static func getCookie() -> Promise<Bool> {
return Promise { seal in
if hasCookie() {
seal.fulfill(true)
}
else {
signInForCookie(cookieCallback: { (status, code) in
if status {
return seal.fulfill(true)
}
else {
seal.reject(NSError.init(domain: "Confirmed Error", code: code, userInfo: nil))
}
})
}
}
}
//MARK: - CREATE USER (macOS only)
public static func createUser(email : String, password : String, passwordConfirmation : String, createUserCallback: @escaping (_ status: Bool, _ reason: String, _ errorCode: Int) -> Void) {
UserDefaults.standard.set(APIVersionType.v3API, forKey: Global.kConfirmedAPIVersion) //force upgrade to latest API version on create user
UserDefaults.standard.synchronize()
NotificationCenter.post(name: .switchingAPIVersions)
if let result = Utils.validateCredentialFormat(email: email, password: password, passwordConfirmation: passwordConfirmation) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { //add delays to allow animation
createUserCallback(false, result, Global.kRequestFieldValidationError)
}
return
}
let parameters: Parameters = parametersForValues(email: email, password: password, passwordConfirmation: passwordConfirmation)
Alamofire.request(Global.createUserURL, method: .post, parameters : parameters, headers: headersForRequest()).responseJSON { response in
let serverResponse = processServerResponse(data: response.data)
if let status = response.response?.statusCode, let eCode = serverResponse.code, status == 200, eCode == Global.kEmailNotConfirmed {
DDLogInfo("User created")
Global.keychain[Global.kConfirmedEmail] = email
Global.keychain[Global.kConfirmedPassword] = password
Auth.clearCookies()
signInForCookie(email: email, password: password, cookieCallback: {(_ status: Bool, _ errorCode: Int) -> Void in
createUserCallback(status, "Unknown error.", errorCode)
})
}
else if let eCode = serverResponse.code, eCode != 0 {
signInError = eCode
createUserCallback(false, serverResponse.message != nil ? serverResponse.message! : Global.errorMessageForError(eCode: Global.kUnknownError), eCode)
}
else {
DDLogError("Error creating user")
if let status = response.response?.statusCode {
signInError = status
createUserCallback(false, serverResponse.message != nil ? serverResponse.message! : Global.errorMessageForError(eCode: Global.kUnknownError), status)
return
}
else {
createUserCallback(false, Global.errorMessageForError(eCode: Global.kUnknownError), Global.kUnknownError)
}
}
}
}
//MARK: - RECEIPT METHOD (iOS Only)
/*
* internal method for uploading receipt
* contains actual call
* upload latest receipt to server
* enesures e-mail has latest receipt
*/
public static func uploadNewReceipt(uploadReceiptCallback: @escaping (_ status: Bool, _ reason: String, _ errorCode: Int) -> Void) {
let sessionManager = Alamofire.SessionManager.default
sessionManager.retrier = CookieHandler()
var parameters: Parameters = parametersForValues(authType: Global.kPlatformiOS, authReceipt: Global.keychain[Global.kConfirmedReceiptKey] as String!)
if Global.isVersion(version: .v1API) {
parameters = parametersForValues(platform: Global.kPlatformiOS, receipt: Global.keychain[Global.kConfirmedReceiptKey] as String!)
}
getCookie()
.then { promise in
sessionManager.request(Global.subscriptionReceiptUploadURL, method: .post, parameters : parameters, headers: headersForRequest()).validate().responseJSON()
}
.done { json, response in
if response.response?.statusCode == 200 {
uploadReceiptCallback(true, "Success", 0)
}
else {
uploadReceiptCallback(false, "Unknown error", 1)
DDLogError("Error with upload: \(response.response?.statusCode)")
}
}
.catch { error in
if signInError == Global.kEmailNotConfirmed {
uploadReceiptCallback(false, Global.errorMessageForError(eCode: Global.kEmailNotConfirmed), Global.kEmailNotConfirmed)
}
else {
uploadReceiptCallback(false, Global.errorMessageForError(eCode: Global.kUnknownError), Global.kUnknownError)
signInError = Global.kUnknownError
}
}
}
//MARK: - ADD EMAIL METHODS (iOS Only)
/*
* method to add an e-mail to a user
*/
public static func convertShadowUser(email : String, password : String, passwordConfirmation : String, createUserCallback: @escaping (_ status: Bool, _ reason: String) -> Void) {
if let result = Utils.validateCredentialFormat(email: email, password: password, passwordConfirmation: passwordConfirmation) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { //add delays to allow animation
createUserCallback(false, result)
}
return
}
let parameters = parametersForValues(newEmail: email, newPassword: password)
let sessionManager = Alamofire.SessionManager.default
sessionManager.retrier = CookieHandler()
let waitAtLeast = after(seconds: 1.0)
getCookie()
.then { promise in
sessionManager.request(Global.addEmailToUserURL, method: .post, parameters : parameters, headers: headersForRequest()).responseJSON()
}
.then { json, response in
waitAtLeast.then { return Promise<(json: Any, response: PMKAlamofireDataResponse)>.value((json, response)) }
}
.done { json, response in
let serverResponse = processServerResponse(data: response.data)
if response.response?.statusCode == 200 && serverResponse.code == Global.kEmailNotConfirmed {
Global.keychain[Global.kConfirmedEmail] = email
Global.keychain[Global.kConfirmedPassword] = password
getKey(callback: {(_ status: Bool, _ reason: String, _errorCode: Int) -> Void in
createUserCallback(true, "")
})
}
else {
DDLogError("Response \(String(describing: String.init(data: response.data!, encoding: .utf8)))")
if let eCode = serverResponse.code {
switch eCode {
case Global.kEmailAlreadyUsed:
createUserCallback(false, Global.errorMessageForError(eCode: eCode))
case Global.kReceiptAlreadyUsed:
createUserCallback(false, Global.errorMessageForError(eCode: eCode))
case Global.kRequestFieldValidationError:
createUserCallback(false, serverResponse.message ?? "Unknown error")
default:
createUserCallback(false, Global.errorMessageForError(eCode: eCode))
print("Unrecognized error")
}
}
else {
if let message = serverResponse.message {
createUserCallback(false, message)
}
else {
createUserCallback(false, Global.errorMessageForError(eCode: Global.kUnknownError))
}
}
}
}
.catch { error in
if signInError == Global.kEmailNotConfirmed {
createUserCallback(false, Global.errorMessageForError(eCode: Global.kEmailNotConfirmed))
}
else {
createUserCallback(false, Global.errorMessageForError(eCode: Global.kUnknownError))
signInError = Global.kUnknownError
}
}
}
//MARK: - GET KEY METHODS
/*
* method to get b64 encoded p12 & user id
*/
public static func getKey(callback: @escaping (_ status: Bool, _ reason: String, _ errorCode: Int) -> Void) {
let sessionManager = Alamofire.SessionManager.default
sessionManager.retrier = CookieHandler()
#if os(iOS)
let parameters: Parameters = parametersForValues(platform: Global.kPlatformiOS, certSource: Utils.getSource())
#else
let parameters: Parameters = parametersForValues(platform: Global.kPlatformMac, certSource: Utils.getSource())
#endif
getCookie()
.then { promise in
sessionManager.request(Global.getKeyURL, method: .post, parameters : parameters, headers: headersForRequest()).responseJSON()
}
.done { json, response in
let resp = processServerResponse(data: response.data)
if !Global.isVersion(version: APIVersionType.v3API) { //set for users who have the .co
UserDefaults.standard.set(true, forKey: Global.kIsOnFinalDeprecatedV1V2)
UserDefaults.standard.synchronize()
}
if response.response?.statusCode == 200, let userB64 = resp.b64, let userID = resp.id {
NotificationCenter.default.post(name: .userSignedIn, object: nil)
Global.keychain[Global.kConfirmedP12Key] = userB64
Global.keychain[Global.kConfirmedID] = userID
if Global.isVersion(version: .v3API) {
Auth.extractP12Cert()
}
signInError = Global.kNoError
callback(true, "", 0)
}
else if let errorCode = resp.code {
signInError = errorCode
callback(false, Global.errorMessageForError(eCode: errorCode), errorCode)
}
else if let code = response.response?.statusCode, code != 200 {
callback(false, Global.errorMessageForError(eCode: code), code)
}
else {
signInError = Global.kUnknownError
callback(false, Global.errorMessageForError(eCode: signInError), signInError)
}
}
.catch { error in
let eCode = (error as NSError).code
if eCode == -1200 || eCode == -1001 || eCode == -1009 || eCode == -1004 { //propagate bad Internet instead of switching API
callback(false, error.localizedDescription, Global.kInternetDownError)
signInError = Global.kInternetDownError
}
else if eCode == Global.kEmailNotConfirmed {
callback(false, Global.errorMessageForError(eCode: Global.kEmailNotConfirmed), Global.kEmailNotConfirmed)
}
else {
callback(false, Global.errorMessageForError(eCode: Global.kUnknownError), signInError)
signInError = Global.kUnknownError
}
}
}
//MARK: - ACCOUNT INFORMATION METHODS
/*
* API to get subscription tier from server
*/
public static func getActiveSubscriptions( callback: @escaping (_ hasActiveSubscription: Bool, _ error: Int, _ errorMessage : String, _ response : Array<Dictionary<String, Any>>?) -> Void) {
let sessionManager = Alamofire.SessionManager.default
sessionManager.retrier = CookieHandler()
let parameters: Parameters = parametersForValues()
getCookie()
.then {_ in
sessionManager.request(Global.activeSubscriptionInformationURL, method: .post, parameters : parameters, headers: headersForRequest()).validate().responseJSON()
}
.done { json, response in
if let sub = json as? Array<Dictionary<String, Any>>, !sub.isEmpty {
callback(true, 0, "", sub)
}
else {
callback(false, Global.kMissingPaymentErrorCode, "No active subscriptions.", nil)
}
}
.catch { error in
if signInError == Global.kEmailNotConfirmed {
callback(false, Global.kEmailNotConfirmed, Global.errorMessageForError(eCode: Global.kEmailNotConfirmed), nil)
}
else {
callback(false, Global.kUnknownError, Global.errorMessageForError(eCode: Global.kUnknownError), nil)
signInError = Global.kUnknownError
}
}
}
//MARK: - CLEAR DATA METHODS
/*
* On signout, clear all keychain data, cookies, and turn VPN off
* cycle through all API versions & clear data
* clear v1/v2/v3 choice
*/
public static func signoutUser() {
for i in 1...3 {
Global.keychain[Global.kConfirmedEmail] = nil
Global.keychain[Global.kConfirmedID] = nil
Global.keychain[Global.kConfirmedP12Key] = nil
Global.keychain[Global.kConfirmedPassword] = nil
#if os(iOS)
Global.keychain[Global.kConfirmedReceiptKey] = nil
TunnelsSubscription.isSubscribed = .NotSubscribed
#endif
Auth.incrementAPIVersion() //switch to clear other API as well
}
UserDefaults.standard.removeObject(forKey:Global.kConfirmedAPIVersion)
if let defaults = UserDefaults(suiteName: SharedUtils.userDefaultsSuite) {
defaults.removeObject(forKey: Utils.kActiveProtocol)
defaults.synchronize()
}
VPNController.shared.forceVPNOff()
Auth.clearCookies()
}
/*
* Clear out cookies from local storage
* Clear cached responses (otherwise cookie response from server may be cached instead of re-generated)
* This catches rare case of cookie being removed before expiration on server
* Often used if sign in failed, can force regeneration of cookies
*/
public static func clearCookies() {
URLCache.shared.removeAllCachedResponses()
URLCache.shared = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil)
if let cookieStorage = Alamofire.SessionManager.default.session.configuration.httpCookieStorage {
for cookie in cookieStorage.cookies ?? [] {
cookieStorage.deleteCookie(cookie as HTTPCookie)
}
}
let cstorage = HTTPCookieStorage.shared
if let cookies = cstorage.cookies(for: URL.init(string: "confirmedvpn.com")!) {
for cookie in cookies {
cstorage.deleteCookie(cookie)
}
}
//deprecated v1/v2 code
if let cookies = cstorage.cookies(for: URL.init(string: "confirmedvpn.co")!) {
for cookie in cookies {
cstorage.deleteCookie(cookie)
}
}
Alamofire.SessionManager.default.session.configuration.httpCookieStorage?.removeCookies(since: Date.init(timeIntervalSinceNow: -1000))
CookieHandler.cookieAuthenticated = false
}
//MARK: - HELPER FUNCTIONS
private static func headersForRequest() -> HTTPHeaders {
var headers = [String:String]()
headers["Confirmed-App-Platform"] = "iOS"
if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
headers["Confirmed-App-Version"] = appVersion
}
Alamofire.SessionManager.default.session.configuration.httpAdditionalHeaders = headers
return headers
}
/*
* Single place for creating post data
*/
private static func parametersForValues(email : String? = nil, password : String? = nil, passwordConfirmation : String? = nil, platform : String? = nil, authType : String? = nil, authReceipt : String? = nil, receipt : String? = nil, newEmail : String? = nil, newPassword : String? = nil, certSource : String? = nil) -> Parameters {
var parameters: Parameters = [:]
if email != nil { parameters["email"] = email }
if password != nil { parameters["password"] = password }
if passwordConfirmation != nil { parameters["passwordConfirmation"] = passwordConfirmation }
if platform != nil { parameters["platform"] = platform }
if authType != nil { parameters["authtype"] = authType }
if authReceipt != nil { parameters["authreceipt"] = authReceipt }
if receipt != nil { parameters["receipt"] = receipt }
if newEmail != nil { parameters["newemail"] = newEmail }
if newPassword != nil { parameters["newpassword"] = newPassword }
if certSource != nil { parameters["source"] = certSource }
return parameters
}
/*
* Convert JSON to a structure
*/
private static func processServerResponse(data : Data?) -> ServerResponse {
if let serverData = data {
let decoder = JSONDecoder()
do {
let resp = try decoder.decode(ServerResponse.self, from: serverData)
return resp
}
catch let parsingError {
print("Error", parsingError)
}
}
let resp = ServerResponse.init(code: Global.kUnknownError, message: nil, b64: nil, id: nil)
return resp
}
}