|
| 1 | +import AutomatticTracks |
| 2 | +import Foundation |
| 3 | + |
| 4 | +/// Full copy of the same file from WP-iOS |
| 5 | +/// Couple of crash logging calls replaced with WC-iOS counterparts |
| 6 | +/// https://github.com/wordpress-mobile/WordPress-iOS/blob/9b1e03b7b89db0eff3075e4460de5c78280f89de/WordPress/Classes/Services/AuthenticationService.swift |
| 7 | +/// |
| 8 | +class AuthenticationService { |
| 9 | + |
| 10 | + static let wpComLoginEndpoint = "https://wordpress.com/wp-login.php" |
| 11 | + |
| 12 | + enum RequestAuthCookieError: Error, LocalizedError { |
| 13 | + case wpcomCookieNotReturned |
| 14 | + |
| 15 | + public var errorDescription: String? { |
| 16 | + switch self { |
| 17 | + case .wpcomCookieNotReturned: |
| 18 | + return "Response to request for auth cookie for WP.com site failed to return cookie." |
| 19 | + } |
| 20 | + } |
| 21 | + } |
| 22 | + |
| 23 | + // MARK: - Self Hosted |
| 24 | + |
| 25 | + func loadAuthCookiesForSelfHosted( |
| 26 | + into cookieJar: CookieJar, |
| 27 | + loginURL: URL, |
| 28 | + username: String, |
| 29 | + password: String, |
| 30 | + success: @escaping () -> Void, |
| 31 | + failure: @escaping (Error) -> Void) { |
| 32 | + |
| 33 | + cookieJar.hasWordPressSelfHostedAuthCookie(for: loginURL, username: username) { hasCookie in |
| 34 | + guard !hasCookie else { |
| 35 | + success() |
| 36 | + return |
| 37 | + } |
| 38 | + |
| 39 | + self.getAuthCookiesForSelfHosted(loginURL: loginURL, username: username, password: password, success: { cookies in |
| 40 | + cookieJar.setCookies(cookies) { |
| 41 | + success() |
| 42 | + } |
| 43 | + |
| 44 | + cookieJar.hasWordPressSelfHostedAuthCookie(for: loginURL, username: username) { hasCookie in |
| 45 | + print("Has cookie: \(hasCookie)") |
| 46 | + } |
| 47 | + }) { error in |
| 48 | + // Make sure this error scenario isn't silently ignored. |
| 49 | + ServiceLocator.crashLogging.logError(error) |
| 50 | + |
| 51 | + // Even if getting the auth cookies fail, we'll still try to load the URL |
| 52 | + // so that the user sees a reasonable error situation on screen. |
| 53 | + // We could opt to create a special screen but for now I'd rather users report |
| 54 | + // the issue when it happens. |
| 55 | + failure(error) |
| 56 | + } |
| 57 | + } |
| 58 | + } |
| 59 | + |
| 60 | + func getAuthCookiesForSelfHosted( |
| 61 | + loginURL: URL, |
| 62 | + username: String, |
| 63 | + password: String, |
| 64 | + success: @escaping (_ cookies: [HTTPCookie]) -> Void, |
| 65 | + failure: @escaping (Error) -> Void) { |
| 66 | + |
| 67 | + let headers = [String: String]() |
| 68 | + let parameters = [ |
| 69 | + "log": username, |
| 70 | + "pwd": password, |
| 71 | + "rememberme": "true" |
| 72 | + ] |
| 73 | + |
| 74 | + requestAuthCookies( |
| 75 | + from: loginURL, |
| 76 | + headers: headers, |
| 77 | + parameters: parameters, |
| 78 | + success: success, |
| 79 | + failure: failure) |
| 80 | + } |
| 81 | + |
| 82 | + // MARK: - WP.com |
| 83 | + |
| 84 | + func loadAuthCookiesForWPCom( |
| 85 | + into cookieJar: CookieJar, |
| 86 | + username: String, |
| 87 | + authToken: String, |
| 88 | + success: @escaping () -> Void, |
| 89 | + failure: @escaping (Error) -> Void) { |
| 90 | + |
| 91 | + cookieJar.hasWordPressComAuthCookie( |
| 92 | + username: username, |
| 93 | + atomicSite: false) { hasCookie in |
| 94 | + |
| 95 | + guard !hasCookie else { |
| 96 | + // The stored cookie can be stale but we'll try to use it and refresh it if the request fails. |
| 97 | + success() |
| 98 | + return |
| 99 | + } |
| 100 | + |
| 101 | + self.getAuthCookiesForWPCom(username: username, authToken: authToken, success: { cookies in |
| 102 | + cookieJar.setCookies(cookies) { |
| 103 | + |
| 104 | + cookieJar.hasWordPressComAuthCookie(username: username, atomicSite: false) { hasCookie in |
| 105 | + guard hasCookie else { |
| 106 | + failure(RequestAuthCookieError.wpcomCookieNotReturned) |
| 107 | + return |
| 108 | + } |
| 109 | + success() |
| 110 | + } |
| 111 | + |
| 112 | + } |
| 113 | + }) { error in |
| 114 | + // Make sure this error scenario isn't silently ignored. |
| 115 | + ServiceLocator.crashLogging.logError(error) |
| 116 | + |
| 117 | + // Even if getting the auth cookies fail, we'll still try to load the URL |
| 118 | + // so that the user sees a reasonable error situation on screen. |
| 119 | + // We could opt to create a special screen but for now I'd rather users report |
| 120 | + // the issue when it happens. |
| 121 | + failure(error) |
| 122 | + } |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + func getAuthCookiesForWPCom( |
| 127 | + username: String, |
| 128 | + authToken: String, |
| 129 | + success: @escaping (_ cookies: [HTTPCookie]) -> Void, |
| 130 | + failure: @escaping (Error) -> Void) { |
| 131 | + |
| 132 | + let loginURL = URL(string: AuthenticationService.wpComLoginEndpoint)! |
| 133 | + let headers = [ |
| 134 | + "Authorization": "Bearer \(authToken)" |
| 135 | + ] |
| 136 | + let parameters = [ |
| 137 | + "log": username, |
| 138 | + "rememberme": "true" |
| 139 | + ] |
| 140 | + |
| 141 | + requestAuthCookies( |
| 142 | + from: loginURL, |
| 143 | + headers: headers, |
| 144 | + parameters: parameters, |
| 145 | + success: success, |
| 146 | + failure: failure) |
| 147 | + } |
| 148 | + |
| 149 | + // MARK: - Request Construction |
| 150 | + |
| 151 | + private func requestAuthCookies( |
| 152 | + from url: URL, |
| 153 | + headers: [String: String], |
| 154 | + parameters: [String: String], |
| 155 | + success: @escaping (_ cookies: [HTTPCookie]) -> Void, |
| 156 | + failure: @escaping (Error) -> Void) { |
| 157 | + |
| 158 | + // We don't want these cookies persisted in other sessions |
| 159 | + let session = URLSession(configuration: .ephemeral) |
| 160 | + var request = URLRequest(url: url) |
| 161 | + |
| 162 | + request.httpMethod = "POST" |
| 163 | + request.httpBody = body(withParameters: parameters) |
| 164 | + |
| 165 | + headers.forEach { (key, value) in |
| 166 | + request.setValue(value, forHTTPHeaderField: key) |
| 167 | + } |
| 168 | +// request.setValue(WPUserAgent.wordPress(), forHTTPHeaderField: "User-Agent") |
| 169 | + |
| 170 | + let task = session.dataTask(with: request) { data, response, error in |
| 171 | + if let error = error { |
| 172 | + DispatchQueue.main.async { |
| 173 | + failure(error) |
| 174 | + } |
| 175 | + return |
| 176 | + } |
| 177 | + |
| 178 | + // The following code is a bit complicated to read, apologies. |
| 179 | + // We're retrieving all cookies from the "Set-Cookie" header manually, and combining |
| 180 | + // those cookies with the ones from the current session. The reason behind this is that |
| 181 | + // iOS's URLSession processes the cookies from such header before this callback is executed, |
| 182 | + // whereas OHTTPStubs.framework doesn't (the cookies are left in the header fields of |
| 183 | + // the response). The only way to combine both is to just add them together here manually. |
| 184 | + // |
| 185 | + // To know if you can remove this, you'll have to test this code live and in our unit tests |
| 186 | + // and compare the session cookies. |
| 187 | + let responseCookies = self.cookies(from: response, loginURL: url) |
| 188 | + let cookies = (session.configuration.httpCookieStorage?.cookies ?? [HTTPCookie]()) + responseCookies |
| 189 | + DispatchQueue.main.async { |
| 190 | + success(cookies) |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + task.resume() |
| 195 | + } |
| 196 | + |
| 197 | + private func body(withParameters parameters: [String: String]) -> Data? { |
| 198 | + var queryItems = [URLQueryItem]() |
| 199 | + |
| 200 | + for parameter in parameters { |
| 201 | + let queryItem = URLQueryItem(name: parameter.key, value: parameter.value) |
| 202 | + queryItems.append(queryItem) |
| 203 | + } |
| 204 | + |
| 205 | + var components = URLComponents() |
| 206 | + components.queryItems = queryItems |
| 207 | + |
| 208 | + return components.percentEncodedQuery?.data(using: .utf8) |
| 209 | + } |
| 210 | + |
| 211 | + // MARK: - Response Parsing |
| 212 | + |
| 213 | + private func cookies(from response: URLResponse?, loginURL: URL) -> [HTTPCookie] { |
| 214 | + guard let httpResponse = response as? HTTPURLResponse, |
| 215 | + let headers = httpResponse.allHeaderFields as? [String: String] else { |
| 216 | + return [] |
| 217 | + } |
| 218 | + |
| 219 | + return HTTPCookie.cookies(withResponseHeaderFields: headers, for: loginURL) |
| 220 | + } |
| 221 | +} |
0 commit comments