Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Commit

Permalink
Fix #6357: Support v2 SKU credentials and add expiration check logic (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
iccub authored Nov 24, 2022
1 parent c304dd8 commit fc54888
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 94 deletions.
2 changes: 2 additions & 0 deletions App/iOS/Delegates/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
if Preferences.URP.referralLookupOutstanding.value == false {
appDelegate.dau.sendPingToServer()
}

BraveSkusManager.refreshSKUCredential(isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing)
}

func sceneWillResignActive(_ scene: UIScene) {
Expand Down
158 changes: 158 additions & 0 deletions Client/BraveSkus/BraveSkusManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright 2022 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

import Foundation
import Shared
import BraveShared
import BraveCore
import BraveVPN
import os.log

public class BraveSkusManager {
private let sku: SkusSkusService

public init?(isPrivateMode: Bool) {
guard let skusService = Skus.SkusServiceFactory.get(privateMode: isPrivateMode) else {
assert(isPrivateMode, "SkusServiceFactory failed to intialize in regular mode, something is wrong.")
return nil
}

self.sku = skusService
}

public static func refreshSKUCredential(isPrivate: Bool) {
guard let _ = Preferences.VPN.skusCredential.value,
let domain = Preferences.VPN.skusCredentialDomain.value,
let expirationDate = Preferences.VPN.expirationDate.value else {
Logger.module.debug("No skus credentials stored in the app.")
return
}

guard expirationDate < Date() else {
Logger.module.debug("Existing sku credential has not expired yet, no need to refresh it.")
return
}

guard let manager = BraveSkusManager(isPrivateMode: isPrivate) else {
return
}

Logger.module.debug("Refreshing sku credential. Clearing old credential from persistence.")

BraveVPN.clearSkusCredentials()

manager.credentialSummary(for: domain) { completion in
Logger.module.debug("credentialSummary response")
}
}

// MARK: - Handling SKU methods.

func refreshOrder(for orderId: String, domain: String, resultJSON: @escaping (Any?) -> Void) {
sku.refreshOrder(domain, orderId: orderId) { completion in
do {
guard let data = completion.data(using: .utf8) else { return }
let json = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed)
Logger.module.debug("refreshOrder json parsed successfully")
resultJSON(json)
} catch {
resultJSON(nil)
Logger.module.error("refrshOrder: Failed to decode json: \(error.localizedDescription)")
}
}
}

func fetchOrderCredentials(for orderId: String, domain: String, resultCredential: @escaping (String) -> Void) {
sku.fetchOrderCredentials(domain, orderId: orderId) { completion in
Logger.module.debug("skus fetchOrderCredentials")
resultCredential(completion)
}
}

func prepareCredentialsPresentation(for domain: String, path: String,
resultCredential: ((String) -> Void)?) {
Logger.module.debug("skus prepareCredentialsPresentation")
sku.prepareCredentialsPresentation(domain, path: path) { credential in
if !credential.isEmpty {
if let vpnCredential = BraveSkusWebHelper.fetchVPNCredential(credential, domain: domain) {
Preferences.VPN.skusCredential.value = credential
Preferences.VPN.skusCredentialDomain.value = domain
Preferences.VPN.expirationDate.value = vpnCredential.expirationDate

BraveVPN.setCustomVPNCredential(vpnCredential)
}
} else {
Logger.module.debug("skus empty credential from prepareCredentialsPresentation call")
}

resultCredential?(credential)
}
}

func credentialSummary(for domain: String, resultJSON: @escaping (Any?) -> Void) {
sku.credentialSummary(domain) { [weak self] completion in
do {
Logger.module.debug("skus credentialSummary")

guard let data = completion.data(using: .utf8) else {
resultJSON(nil)
return
}
let json = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed)

let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
let credentialSummaryJson = try jsonDecoder.decode(CredentialSummary.self, from: data)

if credentialSummaryJson.isValid {

if Preferences.VPN.skusCredential.value == nil {
Logger.module.debug("The credential does NOT exists, calling prepareCredentialsPresentation")
self?.prepareCredentialsPresentation(for: domain, path: "*", resultCredential: nil)
} else {
Logger.module.debug("The credential exists, NOT calling prepareCredentialsPresentation")
}
} else {
if !credentialSummaryJson.active {
Logger.module.debug("The credential summary is not active")
}

if credentialSummaryJson.remainingCredentialCount <= 0 {
Logger.module.debug("The credential summary does not have any remaining credentials")
}
}

resultJSON(json)
} catch {
resultJSON(nil)
Logger.module.error("refrshOrder: Failed to decode json: \(error.localizedDescription)")
}
}
}

private struct CredentialSummary: Codable {
let expiresAt: Date
let active: Bool
let remainingCredentialCount: Int
// The json for credential summary has additional fields. They are not used in the app at the moment.

var isValid: Bool {
active && remainingCredentialCount > 0
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.active = try container.decode(Bool.self, forKey: .active)
self.remainingCredentialCount = try container.decode(Int.self, forKey: .remainingCredentialCount)
guard let expiresAt =
BraveSkusWebHelper.milisecondsOptionalDate(from: try container.decode(String.self, forKey: .expiresAt)) else {
throw DecodingError.typeMismatch(Data.self, .init(codingPath: [],
debugDescription: "Failed to decode Data from String"))
}

self.expiresAt = expiresAt
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Foundation
import Shared
import BraveShared
import os.log
import BraveVPN

class BraveSkusWebHelper {
/// On which hosts the receipt should be allowed to be exposed.
Expand Down Expand Up @@ -102,18 +103,23 @@ class BraveSkusWebHelper {
}

/// Takes credential passed from the Brave SKUs and extract a proper credential to pass to the GuardianConnect framework.
static func fetchVPNCredential(_ credential: String, domain: String) -> (credential: String, environment: String)? {
static func fetchVPNCredential(_ credential: String, domain: String) -> SkusVPNCredential? {
guard let unescapedCredential = credential.unescape(),
let env = environment(domain: domain),
let sampleUrl = URL(string: "https://brave.com") else { return nil }

guard let guardianConnectCredential =
HTTPCookie.cookies(withResponseHeaderFields:
["Set-Cookie": unescapedCredential], for: sampleUrl).first?.value else {
guard let cookie = HTTPCookie.cookies(withResponseHeaderFields:
["Set-Cookie": unescapedCredential], for: sampleUrl).first else {
return nil
}

return (guardianConnectCredential, env)
let guardianCredential = cookie.value

guard let expirationDate = cookie.expiresDate else {
return nil
}

return .init(guardianCredential: guardianCredential, environment: env, expirationDate: expirationDate)
}

static func milisecondsOptionalDate(from stringDate: String) -> Date? {
Expand Down
5 changes: 4 additions & 1 deletion Client/Frontend/Browser/BrowserViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2397,7 +2397,6 @@ extension BrowserViewController: TabDelegate {
BraveTalkScriptHandler(tab: tab, rewards: rewards, launchNativeBraveTalk: { [weak self] tab, room, token in
self?.launchNativeBraveTalk(tab: tab, room: room, token: token)
}),
BraveSkusScriptHandler(tab: tab),
ResourceDownloadScriptHandler(tab: tab),
DownloadContentScriptHandler(browserController: self, tab: tab),
WindowRenderScriptHandler(tab: tab),
Expand All @@ -2414,6 +2413,10 @@ extension BrowserViewController: TabDelegate {
tab.requestBlockingContentHelper,
]

if let braveSkusHandler = BraveSkusScriptHandler(tab: tab) {
injectedScripts.append(braveSkusHandler)
}

// Only add the logins handler and wallet provider if the tab is NOT a private browsing tab
if !tab.isPrivate {
injectedScripts += [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import os.log
class BraveSkusScriptHandler: TabContentScript {
typealias ReplyHandler = (Any?, String?) -> Void

private weak var tab: Tab?
private let sku: SkusSkusService?
private let braveSkusManager: BraveSkusManager

required init(tab: Tab) {
self.tab = tab
self.sku = Skus.SkusServiceFactory.get(privateMode: tab.isPrivate)
required init?(tab: Tab) {
guard let manager = BraveSkusManager(isPrivateMode: tab.isPrivate) else {
return nil
}

self.braveSkusManager = manager
}

static let scriptName = "BraveSkusScript"
Expand All @@ -45,7 +47,9 @@ class BraveSkusScriptHandler: TabContentScript {
case credentialsSummary = 4
}

func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void) {
func userContentController(_ userContentController: WKUserContentController,
didReceiveScriptMessage message: WKScriptMessage,
replyHandler: @escaping (Any?, String?) -> Void) {
if !verifyMessage(message: message) {
assertionFailure("Missing required security token.")
return
Expand All @@ -70,87 +74,27 @@ class BraveSkusScriptHandler: TabContentScript {
switch methodId {
case Method.refreshOrder.rawValue:
if let orderId = data["orderId"] as? String {
handleRefreshOrder(for: orderId, domain: requestHost, replyHandler: replyHandler)

braveSkusManager.refreshOrder(for: orderId, domain: requestHost) { result in
replyHandler(result, nil)
}
}
case Method.fetchOrderCredentials.rawValue:
if let orderId = data["orderId"] as? String {
handleFetchOrderCredentials(for: orderId, domain: requestHost, replyHandler: replyHandler)
braveSkusManager.fetchOrderCredentials(for: orderId, domain: requestHost) { result in
replyHandler(result, nil)
}
}
case Method.prepareCredentialsPresentation.rawValue:
if let domain = data["domain"] as? String, let path = data["path"] as? String {
handlePrepareCredentialsSummary(for: domain, path: path, replyHandler: replyHandler)
}
assertionFailure("The website should never call the credentialsPresentation.")
case Method.credentialsSummary.rawValue:
if let domain = data["domain"] as? String {
handleCredentialsSummary(for: domain, replyHandler: replyHandler)
braveSkusManager.credentialSummary(for: domain) { result in
replyHandler(result, nil)
}
}
default:
assertionFailure("Failure, the website called unhandled method with id: \(methodId)")
}
}

private func handleRefreshOrder(for orderId: String, domain: String, replyHandler: @escaping ReplyHandler) {
sku?.refreshOrder(domain, orderId: orderId) { completion in
do {
guard let data = completion.data(using: .utf8) else { return }
let json = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed)
Logger.module.debug("skus refreshOrder")
replyHandler(json, nil)
} catch {
replyHandler("", nil)
Logger.module.error("refrshOrder: Failed to decode json: \(error.localizedDescription)")
}
}
}

private func handleFetchOrderCredentials(for orderId: String, domain: String, replyHandler: @escaping ReplyHandler) {
sku?.fetchOrderCredentials(domain, orderId: orderId) { completion in
Logger.module.debug("skus fetchOrderCredentials")
replyHandler(completion, nil)
}
}

/// If no reply handler is passed, this function will not send the callback back to the website.
/// Reason is this method may be called from within another web handler, and the callback can be called only once or it crashes.
private func handlePrepareCredentialsSummary(for domain: String, path: String, replyHandler: ReplyHandler?) {
Logger.module.debug("skus prepareCredentialsPresentation")
sku?.prepareCredentialsPresentation(domain, path: path) { credential in
if !credential.isEmpty {
if let vpnCredential = BraveSkusWebHelper.fetchVPNCredential(credential, domain: domain) {
Preferences.VPN.skusCredential.value = credential
Preferences.VPN.skusCredentialDomain.value = domain

BraveVPN.setCustomVPNCredential(vpnCredential.credential, environment: vpnCredential.environment)
}
} else {
Logger.module.debug("skus empty credential from prepareCredentialsPresentation call")
}

replyHandler?(credential, nil)
}
}

private func handleCredentialsSummary(for domain: String, replyHandler: @escaping ReplyHandler) {
sku?.credentialSummary(domain) { [weak self] completion in
do {
Logger.module.debug("skus credentialSummary")

guard let data = completion.data(using: .utf8) else { return }
let json = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed)

replyHandler(json, nil)

if let expiresDate = (json as? [String: Any])?["expires_at"] as? String,
let date = BraveSkusWebHelper.milisecondsOptionalDate(from: expiresDate) {
Preferences.VPN.expirationDate.value = date
} else {
assertionFailure("Failed to parse date")
}

self?.handlePrepareCredentialsSummary(for: domain, path: "*", replyHandler: nil)
} catch {
Logger.module.error("refrshOrder: Failed to decode json: \(error.localizedDescription)")
}
}
}
}
Loading

0 comments on commit fc54888

Please sign in to comment.