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

Fix #6357: Support v2 SKU credentials and add expiration check logic #6397

Merged
merged 15 commits into from
Nov 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions App/iOS/Delegates/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
if Preferences.URP.referralLookupOutstanding.value == false {
appDelegate.dau.sendPingToServer()
}

BraveSkusManager.refreshSKUCredential(isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing)
bsclifton marked this conversation as resolved.
Show resolved Hide resolved
kylehickinson marked this conversation as resolved.
Show resolved Hide resolved
}

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
bsclifton marked this conversation as resolved.
Show resolved Hide resolved
}

guard expirationDate < Date() else {
Logger.module.debug("Existing sku credential has not expired yet, no need to refresh it.")
bsclifton marked this conversation as resolved.
Show resolved Hide resolved
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)
iccub marked this conversation as resolved.
Show resolved Hide resolved
} 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
bsclifton marked this conversation as resolved.
Show resolved Hide resolved
// The json for credential summary has additional fields. They are not used in the app at the moment.

var isValid: Bool {
active && remainingCredentialCount > 0
iccub marked this conversation as resolved.
Show resolved Hide resolved
}

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
iccub marked this conversation as resolved.
Show resolved Hide resolved
}

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 @@ -2395,7 +2395,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 @@ -2412,6 +2411,10 @@ extension BrowserViewController: TabDelegate {
tab.requestBlockingContentHelper,
]

if let braveSkusHandler = BraveSkusScriptHandler(tab: tab) {
injectedScripts.append(braveSkusHandler)
iccub marked this conversation as resolved.
Show resolved Hide resolved
}

// 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.")
iccub marked this conversation as resolved.
Show resolved Hide resolved
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