Skip to content

Commit

Permalink
Fix brave/brave-ios#6225: Ethereum Name Service (ENS) Navigation (bra…
Browse files Browse the repository at this point in the history
…ve/brave-ios#7153)

* Add ENS Resolve Method preference to `Web3DomainSettingsView`

* Refactor `SNSDomain.html`, `SNSDomain.css`, `SNSDomainHandler`, `Web3NameServiceScriptHandler` to be more generic. Support for SNS, ENS and ENS Offchain (and later Unstoppable Domains can re-use).
Integrate initial support for ENS navigation via omnibar (supported until IPFS url received).

* Add new `DecentralizedDNSHelper` to contain decentralized DNS logic to `BraveWallet` target

* Add `DecentralizedDNSHelperTests` for unit testing `DecentralizedDNSHelper`. Update `SettingsStoreTests` for testing `ensResolveMethod` in setup.

* Close keyboard / leave overlay mode when resolving decentralized DNS. Update selected tab loading state when user presses stop.

* Use a single `Web3DomainHandler` by storing service id in url query.

* Verify URL is not a bookmarklet / javascript before returning in `DecentralizedDNSHelper`.

* Support decentralized DNS links when tapped on a website (main-frame only).
  • Loading branch information
StephenHeaps authored Apr 6, 2023
1 parent 331185a commit cb7cfc3
Show file tree
Hide file tree
Showing 20 changed files with 750 additions and 143 deletions.
7 changes: 4 additions & 3 deletions App/iOS/Delegates/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import BraveTalk
#endif
import Onboarding
import os
import BraveWallet

extension AppDelegate {
// A model that is passed used in every scene
Expand Down Expand Up @@ -418,10 +419,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
(SessionRestoreHandler.path, SessionRestoreHandler()),
(ErrorPageHandler.path, ErrorPageHandler()),
(ReaderModeHandler.path, ReaderModeHandler(profile: profile)),
(SNSDomainHandler.path, SNSDomainHandler()),
(IPFSSchemeHandler.path, IPFSSchemeHandler())
(IPFSSchemeHandler.path, IPFSSchemeHandler()),
(Web3DomainHandler.path, Web3DomainHandler())
]

responders.forEach { (path, responder) in
InternalSchemeHandler.responders[path] = responder
}
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ var braveTarget: PackageDescription.Target = .target(
.copy("Assets/Interstitial Pages/Pages/CertificateError.html"),
.copy("Assets/Interstitial Pages/Pages/GenericError.html"),
.copy("Assets/Interstitial Pages/Pages/NetworkError.html"),
.copy("Assets/Interstitial Pages/Pages/SNSDomain.html"),
.copy("Assets/Interstitial Pages/Pages/Web3Domain.html"),
.copy("Assets/Interstitial Pages/Pages/IPFSPreference.html"),
.copy("Assets/Interstitial Pages/Images/Carret.png"),
.copy("Assets/Interstitial Pages/Images/Clock.svg"),
Expand All @@ -383,7 +383,7 @@ var braveTarget: PackageDescription.Target = .target(
.copy("Assets/Interstitial Pages/Styles/CertificateError.css"),
.copy("Assets/Interstitial Pages/Styles/InterstitialStyles.css"),
.copy("Assets/Interstitial Pages/Styles/NetworkError.css"),
.copy("Assets/Interstitial Pages/Styles/SNSDomain.css"),
.copy("Assets/Interstitial Pages/Styles/Web3Domain.css"),
.copy("Assets/Interstitial Pages/Styles/IPFSPreference.css"),
.copy("Assets/SearchPlugins"),
.copy("Frontend/Reader/Reader.css"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<meta name="referrer" content="no-referrer">
<title>%page_title%</title>
<link rel="stylesheet" href="internal://local/interstitial-style/InterstitialStyles.css">
<link rel="stylesheet" href="internal://local/interstitial-style/SNSDomain.css">
<link rel="stylesheet" href="internal://local/interstitial-style/Web3Domain.css">
</head>

<body dir="&locale.dir;" class="background content">
Expand All @@ -28,23 +28,25 @@
<br />

<div id="navigationButtons" class="navigationButtons">
<button id="disableSNSButton" class="disableButton disableButtonTitle">%button_disable%</button>
<button id="proceedSNSButton" class="proceedButton proceedButtonTitle">%button_procced%</button>
<button id="disableWeb3Button" class="disableButton disableButtonTitle">%button_disable%</button>
<button id="proceedWeb3Button" class="proceedButton proceedButtonTitle">%button_procced%</button>
</div>

<script type="text/javascript">
var disableButton = document.getElementById("disableSNSButton")
var disableButton = document.getElementById("disableWeb3Button")
disableButton.addEventListener('click', function(e) {
e.preventDefault();
webkit.messageHandlers["%message_handler%"].postMessage({
"type": "SNSDisable",
"button_type": "disable",
"service_id": "%service_id%"
});
});
var proceedButton = document.getElementById("proceedSNSButton")
var proceedButton = document.getElementById("proceedWeb3Button")
proceedButton.addEventListener('click', function(e) {
e.preventDefault();
webkit.messageHandlers["%message_handler%"].postMessage({
"type": "SNSProceed",
"button_type": "proceed",
"service_id": "%service_id%"
});
});

Expand Down
11 changes: 8 additions & 3 deletions Sources/Brave/Frontend/Browser/BrowserViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ public class BrowserViewController: UIViewController {
weak var walletStore: WalletStore?

var lastEnteredURLVisitType: VisitType = .unknown

var processAddressBarTask: Task<(), Never>?
var topToolbarDidPressReloadTask: Task<(), Never>?

public init(
profile: Profile,
Expand Down Expand Up @@ -1607,10 +1610,12 @@ public class BrowserViewController: UIViewController {
tab.webView?.load(PrivilegedRequest(url: internalUrl) as URLRequest)
}

func showSNSDomainInterstitialPage(originalURL: URL, visitType: VisitType) {
func showWeb3ServiceInterstitialPage(service: Web3Service, originalURL: URL, visitType: VisitType = .unknown) {
topToolbar.leaveOverlayMode()

guard let tab = tabManager.selectedTab, let encodedURL = originalURL.absoluteString.addingPercentEncoding(withAllowedCharacters: .alphanumerics), let internalUrl = URL(string: "\(InternalURL.baseUrl)/\(SNSDomainHandler.path)?url=\(encodedURL)") else {

guard let tab = tabManager.selectedTab,
let encodedURL = originalURL.absoluteString.addingPercentEncoding(withAllowedCharacters: .alphanumerics),
let internalUrl = URL(string: "\(InternalURL.baseUrl)/\(Web3DomainHandler.path)?\(Web3NameServiceScriptHandler.ParamKey.serviceId.rawValue)=\(service.rawValue)&url=\(encodedURL)") else {
return
}
let scriptHandler = tab.getContentScript(name: Web3NameServiceScriptHandler.scriptName) as? Web3NameServiceScriptHandler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,29 +107,28 @@ extension BrowserViewController: TopToolbarDelegate {
}

func topToolbarDidPressReload(_ topToolbar: TopToolbarView) {
let isPrivateMode = PrivateBrowsingManager.shared.isPrivateBrowsing

if let url = topToolbar.currentURL {
if url.isIPFSScheme {
if !handleIPFSSchemeURL(url, visitType: .unknown) {
tabManager.selectedTab?.reload()
}
} else if !isPrivateMode, url.domainURL.schemelessAbsoluteDisplayString.endsWithSupportedSNSExtension, let rpcService = BraveWallet.JsonRpcServiceFactory.get(privateMode: isPrivateMode) {
Task { @MainActor in
let currentStatus = await rpcService.snsResolveMethod()
switch currentStatus {
case .ask:
// show name service interstitial page
showSNSDomainInterstitialPage(originalURL: url, visitType: .unknown)
case .enabled:
if let resolvedURL = await resolveSNSHost(url.schemelessAbsoluteDisplayString, rpcService: rpcService) {
} else if let decentralizedDNSHelper = decentralizedDNSHelperFor(url: topToolbar.currentURL) {
topToolbarDidPressReloadTask?.cancel()
topToolbarDidPressReloadTask = Task { @MainActor in
topToolbar.locationView.loading = true
let result = await decentralizedDNSHelper.lookup(domain: url.schemelessAbsoluteDisplayString)
topToolbar.locationView.loading = tabManager.selectedTab?.loading ?? false
guard !Task.isCancelled else { return } // user pressed stop, or typed new url
switch result {
case let .loadInterstitial(service):
showWeb3ServiceInterstitialPage(service: service, originalURL: url)
case let .load(resolvedURL):
if resolvedURL.isIPFSScheme {
handleIPFSSchemeURL(resolvedURL, visitType: .unknown)
} else {
tabManager.selectedTab?.loadRequest(URLRequest(url: resolvedURL))
return
}
tabManager.selectedTab?.loadRequest(URLRequest(url: url))
case .disabled:
tabManager.selectedTab?.reload()
@unknown default:
case .none:
tabManager.selectedTab?.reload()
}
}
Expand All @@ -143,6 +142,9 @@ extension BrowserViewController: TopToolbarDelegate {

func topToolbarDidPressStop(_ topToolbar: TopToolbarView) {
tabManager.selectedTab?.stop()
processAddressBarTask?.cancel()
topToolbarDidPressReloadTask?.cancel()
topToolbar.locationView.loading = tabManager.selectedTab?.loading ?? false
}

func topToolbarDidLongPressReloadButton(_ topToolbar: TopToolbarView, from button: UIButton) {
Expand Down Expand Up @@ -246,7 +248,8 @@ extension BrowserViewController: TopToolbarDelegate {
}

func processAddressBar(text: String, visitType: VisitType, isBraveSearchPromotion: Bool = false) {
Task { @MainActor in
processAddressBarTask?.cancel()
processAddressBarTask = Task { @MainActor in
if !isBraveSearchPromotion, await submitValidURL(text, visitType: visitType) {
return
} else {
Expand All @@ -260,12 +263,6 @@ extension BrowserViewController: TopToolbarDelegate {
}
}

@MainActor func resolveSNSHost(_ host: String, rpcService: BraveWalletJsonRpcService) async -> URL? {
let (url, status, _) = await rpcService.snsResolveHost(host)
guard let url = url, status == .success else { return nil }
return url
}

@discardableResult
func handleIPFSSchemeURL(_ url: URL, visitType: VisitType) -> Bool {
guard !PrivateBrowsingManager.shared.isPrivateBrowsing else {
Expand Down Expand Up @@ -307,24 +304,26 @@ extension BrowserViewController: TopToolbarDelegate {
// Do not allow users to enter URLs with the following schemes.
// Instead, submit them to the search engine like Chrome-iOS does.
if !["file"].contains(fixupURL.scheme) {
// check text is SNS domain
let isPrivateMode = PrivateBrowsingManager.shared.isPrivateBrowsing
if !isPrivateMode, fixupURL.domainURL.schemelessAbsoluteDisplayString.endsWithSupportedSNSExtension, let rpcService = BraveWallet.JsonRpcServiceFactory.get(privateMode: isPrivateMode) {
let currentStatus = await rpcService.snsResolveMethod()
switch currentStatus {
case .ask:
// show name service interstitial page
showSNSDomainInterstitialPage(originalURL: fixupURL, visitType: visitType)
// check text is decentralized DNS supported domain
if let decentralizedDNSHelper = self.decentralizedDNSHelperFor(url: fixupURL) {
topToolbar.leaveOverlayMode()
updateToolbarCurrentURL(fixupURL)
topToolbar.locationView.loading = true
let result = await decentralizedDNSHelper.lookup(domain: fixupURL.schemelessAbsoluteDisplayString)
topToolbar.locationView.loading = tabManager.selectedTab?.loading ?? false
guard !Task.isCancelled else { return true } // user pressed stop, or typed new url
switch result {
case let .loadInterstitial(service):
showWeb3ServiceInterstitialPage(service: service, originalURL: fixupURL, visitType: visitType)
return true
case .enabled:
if let resolvedURL = await resolveSNSHost(text, rpcService: rpcService) {
// resolved url
case let .load(resolvedURL):
if resolvedURL.isIPFSScheme {
return handleIPFSSchemeURL(resolvedURL, visitType: visitType)
} else {
finishEditingAndSubmit(resolvedURL, visitType: visitType)
return true
}
case .disabled:
break
@unknown default:
case .none:
break
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ extension BrowserViewController: WKNavigationDelegate {

@MainActor
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences) async -> (WKNavigationActionPolicy, WKWebpagePreferences) {
guard let url = navigationAction.request.url else {
guard var url = navigationAction.request.url else {
return (.cancel, preferences)
}

Expand Down Expand Up @@ -198,6 +198,31 @@ extension BrowserViewController: WKNavigationDelegate {
}
return (.cancel, preferences)
}

// handles Decentralized DNS
if let decentralizedDNSHelper = self.decentralizedDNSHelperFor(url: url),
navigationAction.targetFrame?.isMainFrame == true {
topToolbar.locationView.loading = true
let result = await decentralizedDNSHelper.lookup(domain: url.schemelessAbsoluteDisplayString)
topToolbar.locationView.loading = tabManager.selectedTab?.loading ?? false
guard !Task.isCancelled else { // user pressed stop, or typed new url
return (.cancel, preferences)
}
switch result {
case let .loadInterstitial(service):
showWeb3ServiceInterstitialPage(service: service, originalURL: url, visitType: .link)
return (.cancel, preferences)
case let .load(resolvedURL):
if resolvedURL.isIPFSScheme {
handleIPFSSchemeURL(resolvedURL, visitType: .link)
return (.cancel, preferences)
} else { // non-ipfs, treat as normal url / link tapped
url = resolvedURL
}
case .none:
break
}
}

let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,51 @@ import BraveShared
import BraveCore

extension BrowserViewController: Web3NameServiceScriptHandlerDelegate {
func web3NameServiceDecisionHandler(_ proceed: Bool, originalURL: URL, visitType: VisitType) {
/// Returns a `DecentralizedDNSHelper` for the given mode if supported and not in private mode.
func decentralizedDNSHelperFor(url: URL?) -> DecentralizedDNSHelper? {
let isPrivateMode = PrivateBrowsingManager.shared.isPrivateBrowsing
guard let rpcService = BraveWallet.JsonRpcServiceFactory.get(privateMode: isPrivateMode) else {
guard !isPrivateMode,
let url,
DecentralizedDNSHelper.isSupported(domain: url.domainURL.schemelessAbsoluteDisplayString),
let rpcService = BraveWallet.JsonRpcServiceFactory.get(privateMode: isPrivateMode) else { return nil }
return DecentralizedDNSHelper(
rpcService: rpcService,
ipfsApi: braveCore.ipfsAPI,
isPrivateMode: isPrivateMode
)
}

func web3NameServiceDecisionHandler(_ proceed: Bool, web3Service: Web3Service, originalURL: URL, visitType: VisitType) {
let isPrivateMode = PrivateBrowsingManager.shared.isPrivateBrowsing
guard let rpcService = BraveWallet.JsonRpcServiceFactory.get(privateMode: isPrivateMode),
let decentralizedDNSHelper = self.decentralizedDNSHelperFor(url: originalURL) else {
finishEditingAndSubmit(originalURL, visitType: visitType)
return
}
if proceed {
Task { @MainActor in
rpcService.setSnsResolveMethod(.enabled)
if let host = originalURL.host, let resolvedUrl = await resolveSNSHost(host, rpcService: rpcService) {
// resolved url
finishEditingAndSubmit(resolvedUrl, visitType: visitType)
Task { @MainActor in
switch web3Service {
case .solana:
rpcService.setSnsResolveMethod(proceed ? .enabled : .disabled)
case .ethereum:
rpcService.setEnsResolveMethod(proceed ? .enabled : .disabled)
case .ethereumOffchain:
rpcService.setEnsOffchainLookupResolveMethod(proceed ? .enabled : .disabled)
}
let result = await decentralizedDNSHelper.lookup(domain: originalURL.host ?? originalURL.absoluteString)
switch result {
case let .load(resolvedURL):
if resolvedURL.isIPFSScheme {
handleIPFSSchemeURL(resolvedURL, visitType: visitType)
} else {
finishEditingAndSubmit(resolvedURL, visitType: visitType)
}
case let .loadInterstitial(service):
// ENS interstitial -> ENS Offchain interstitial possible
showWeb3ServiceInterstitialPage(service: service, originalURL: originalURL, visitType: visitType)
case .none:
// failed to resolve domain or disabled
finishEditingAndSubmit(originalURL, visitType: visitType)
}
} else {
rpcService.setSnsResolveMethod(.disabled)
finishEditingAndSubmit(originalURL, visitType: visitType)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class IPFSSchemeHandler: InternalSchemeResponse {
"interstitial_ipfs_title": Strings.Wallet.web3IPFSInterstitialIPFSTitle,
"interstitial_ipfs_privacy": String.localizedStringWithFormat(Strings.Wallet.web3IPFSInterstitialIPFSPrivacy, WalletConstants.ipfsLearnMoreLink.absoluteString, Strings.learnMore.lowercased().capitalizeFirstLetter),
"interstitial_ipfs_public_gateway": Strings.Wallet.web3IPFSInterstitialIPFSPublicGateway,
"button_disable": Strings.Wallet.snsDomainInterstitialPageButtonDisable,
"button_disable": Strings.Wallet.web3DomainInterstitialPageButtonDisable,
"button_procced": Strings.Wallet.web3IPFSInterstitialProceedButton,
"message_handler": Web3IPFSScriptHandler.messageHandlerName,
]
Expand Down
53 changes: 0 additions & 53 deletions Sources/Brave/Frontend/Browser/Handlers/SNSDomainHandler.swift

This file was deleted.

Loading

0 comments on commit cb7cfc3

Please sign in to comment.