diff --git a/add-on/src/landing-pages/protocol-error/protocol-error.css b/add-on/src/landing-pages/protocol-error/protocol-error.css
new file mode 100644
index 000000000..3cc87cb55
--- /dev/null
+++ b/add-on/src/landing-pages/protocol-error/protocol-error.css
@@ -0,0 +1,196 @@
+body {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif;
+ background: #0b3a53;
+ background: linear-gradient(135deg, #0b3a53 0%, #1d4362 100%);
+ color: #ffffff;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 100vh;
+ margin: 0;
+ padding: 20px;
+}
+
+.container {
+ max-width: 600px;
+ background: rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(105, 196, 205, 0.2);
+ border-radius: 8px;
+ padding: 48px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+}
+
+.warning-icon {
+ font-size: 48px;
+ text-align: center;
+ margin-bottom: 24px;
+ filter: sepia(1) saturate(2) hue-rotate(150deg);
+}
+
+h1 {
+ color: #ffffff;
+ font-size: 28px;
+ font-weight: 600;
+ margin: 0 0 24px 0;
+ text-align: center;
+}
+
+.error-message {
+ color: rgba(255, 255, 255, 0.8);
+ font-size: 16px;
+ line-height: 1.6;
+ text-align: center;
+ margin-bottom: 32px;
+}
+
+.error-message code {
+ background: rgba(105, 196, 205, 0.2);
+ color: #69c4cd;
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-family: 'Monaco', 'Courier New', monospace;
+}
+
+.url-display {
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 8px;
+ padding: 24px;
+ margin-bottom: 32px;
+}
+
+.url-row {
+ display: flex;
+ align-items: center;
+ margin-bottom: 16px;
+}
+
+.url-row:last-child {
+ margin-bottom: 0;
+}
+
+.url-label {
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ width: 100px;
+ flex-shrink: 0;
+}
+
+.url-incorrect {
+ color: #f36149;
+ font-family: 'Monaco', 'Courier New', monospace;
+ font-size: 14px;
+ text-decoration: line-through;
+ opacity: 0.8;
+}
+
+.url-correct {
+ color: #69c4cd;
+ font-family: 'Monaco', 'Courier New', monospace;
+ font-size: 14px;
+ font-weight: 600;
+}
+
+.explanation-box {
+ background: rgba(105, 196, 205, 0.1);
+ border-left: 3px solid #69c4cd;
+ border-radius: 4px;
+ padding: 20px;
+ margin-bottom: 32px;
+}
+
+.explanation-title {
+ color: #69c4cd;
+ font-weight: 600;
+ margin-bottom: 12px;
+ font-size: 16px;
+}
+
+.explanation-list {
+ margin: 0;
+ padding-left: 20px;
+ color: rgba(255, 255, 255, 0.8);
+ line-height: 1.8;
+}
+
+.explanation-list code {
+ background: rgba(105, 196, 205, 0.2);
+ color: #69c4cd;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-family: 'Monaco', 'Courier New', monospace;
+ font-size: 13px;
+}
+
+.button-container {
+ display: flex;
+ gap: 16px;
+ margin-bottom: 24px;
+}
+
+.btn {
+ flex: 1;
+ padding: 14px 24px;
+ font-size: 16px;
+ font-weight: 500;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ font-family: inherit;
+}
+
+.btn-back {
+ background: rgba(255, 255, 255, 0.1);
+ color: rgba(255, 255, 255, 0.8);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+.btn-back:hover {
+ background: rgba(255, 255, 255, 0.15);
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+.btn-continue {
+ background: #69c4cd;
+ color: #0b3a53;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+}
+
+.btn-continue:hover {
+ background: #7dd0d8;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(105, 196, 205, 0.3);
+}
+
+.arrow {
+ font-size: 20px;
+ transition: transform 0.2s ease;
+}
+
+.btn-continue:hover .arrow {
+ transform: translateX(4px);
+}
+
+.learn-more {
+ text-align: center;
+ margin-top: 0;
+}
+
+.learn-more a {
+ color: #69c4cd;
+ text-decoration: none;
+ font-size: 14px;
+ transition: opacity 0.2s ease;
+}
+
+.learn-more a:hover {
+ opacity: 0.8;
+ text-decoration: underline;
+}
\ No newline at end of file
diff --git a/add-on/src/landing-pages/protocol-error/protocol-error.html b/add-on/src/landing-pages/protocol-error/protocol-error.html
new file mode 100644
index 000000000..26c626ec9
--- /dev/null
+++ b/add-on/src/landing-pages/protocol-error/protocol-error.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+ IPFS Protocol Error - IPFS Companion
+
+
+
+
+
⚠️
+
+
Incorrect Protocol Usage
+
+
+ You're trying to access a domain name using ipfs://
protocol,
+ but domain names should use ipns://
instead.
+
+
+
+
+ Incorrect:
+ ipfs://example.com
+
+
+ Correct:
+ ipns://example.com
+
+
+
+
+
Quick Guide
+
+ ipfs://
- Use only for immutable content identifiers (CIDs)
+ ipns://
- Use for mutable pointers like domain names and IPNS records
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/add-on/src/landing-pages/protocol-error/protocol-error.js b/add-on/src/landing-pages/protocol-error/protocol-error.js
new file mode 100644
index 000000000..a0fe1cfd3
--- /dev/null
+++ b/add-on/src/landing-pages/protocol-error/protocol-error.js
@@ -0,0 +1,26 @@
+'use strict';
+/* eslint-env browser, webextensions */
+
+document.addEventListener('DOMContentLoaded', () => {
+ const params = new URLSearchParams(window.location.search)
+ const incorrectUrl = params.get('url') || ''
+
+ if (incorrectUrl) {
+ const domain = incorrectUrl.replace(/^ipfs:\/\//, '')
+ const correctUrl = `ipns://${domain}`
+
+ document.getElementById('incorrect-url').textContent = incorrectUrl
+ document.getElementById('correct-url').textContent = correctUrl
+ document.getElementById('btn-url').textContent = correctUrl
+ document.getElementById('continue-btn').addEventListener('click', () => {
+ // Redirect to Google search with ipns:// which will be intercepted by the extension
+ const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(correctUrl)}`
+ window.location.href = searchUrl
+ })
+
+ // Handle Go Back button
+ document.getElementById('go-back').addEventListener('click', () => {
+ window.history.back()
+ })
+ }
+})
\ No newline at end of file
diff --git a/add-on/src/lib/ipfs-request.js b/add-on/src/lib/ipfs-request.js
index 0662ddb15..295ca1ec9 100644
--- a/add-on/src/lib/ipfs-request.js
+++ b/add-on/src/lib/ipfs-request.js
@@ -183,7 +183,7 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida
}
// poor-mans protocol handlers - https://github.com/ipfs/ipfs-companion/issues/164#issuecomment-328374052
if (state.catchUnhandledProtocols && mayContainUnhandledIpfsProtocol(request)) {
- const fix = await normalizedUnhandledIpfsProtocol(request, state.pubGwURLString)
+ const fix = await normalizedUnhandledIpfsProtocol(request, state.pubGwURLString, runtime)
if (fix) {
return fix
}
@@ -412,7 +412,7 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida
url.protocol = 'https:' // force HTTPS, as HSTS may be missing on initial load
const redirectUrl = url.toString()
log(`onErrorOccurred: attempting to recover from DNS error (${request.error}) using EthDNS for ${request.url} → ${redirectUrl}`, request)
- return updateTabWithURL(request, redirectUrl, browser)
+ return updateTabWithURL(request, redirectUrl, runtime.browser)
}
// Check if error can be recovered via DNSLink
@@ -423,7 +423,7 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida
const redirectUrl = await dnslinkResolver.dnslinkAtGateway(request.url, dnslink)
log(`onErrorOccurred: attempting to recover from network error (${request.error}) using dnslink for ${request.url} → ${redirectUrl}`, request)
// We are unable to redirect in onErrorOccurred, but we can update the tab
- return updateTabWithURL(request, redirectUrl, browser)
+ return updateTabWithURL(request, redirectUrl, runtime.browser)
}
}
@@ -438,7 +438,7 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida
const redirectUrl = await ipfsPathValidator.resolveToPublicUrl(request.url)
log(`onErrorOccurred: attempting to recover from network error (${request.error}) for ${request.url} → ${redirectUrl}`, request)
// We are unable to redirect in onErrorOccurred, but we can update the tab
- return updateTabWithURL(request, redirectUrl, browser)
+ return updateTabWithURL(request, redirectUrl, runtime.browser)
}
},
@@ -468,7 +468,7 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida
if (await isRecoverable(request, state, ipfsPathValidator)) {
const redirectUrl = await ipfsPathValidator.resolveToPublicUrl(request.url)
log(`onCompleted: attempting to recover from HTTP Error ${request.statusCode} for ${request.url} → ${redirectUrl}`, request)
- return updateTabWithURL(request, redirectUrl, browser)
+ return updateTabWithURL(request, redirectUrl, runtime.browser)
}
}
}
@@ -634,6 +634,22 @@ function fixupDnslinkPath (path) {
// Background: https://github.com/ipfs-shipyard/ipfs-companion/issues/164#issuecomment-328374052
// ===================================================================
+function isDomainName (path) {
+ // Check if it's a domain (has dots and is not a CID)
+ if (!path) return false
+
+ // Check for common CID patterns
+ const isCID =
+ path.startsWith('Qm') || // CIDv0
+ path.startsWith('bafy') || // CIDv1
+ path.startsWith('bafk') || // CIDv1
+ path.startsWith('bafz') || // CIDv1
+ path.match(/^[a-z2-7]{59}$/) // base32 CIDv1
+
+ // It's a domain if it has dots and is not a CID
+ return path.includes('.') && !isCID
+}
+
const unhandledIpfsRE = /=(?:web%2B|)(ipfs(?=%3A%2F%2F)|ipns(?=%3A%2F%2F)|dweb(?=%3A%2Fip[f|n]s))%3A(?:%2F%2F|%2F)([^&]+)/
function mayContainUnhandledIpfsProtocol (request) {
@@ -650,19 +666,38 @@ function unhandledIpfsPath (requestUrl) {
return null
}
-function normalizedUnhandledIpfsProtocol (request, pubGwUrl) {
+function normalizedUnhandledIpfsProtocol (request, pubGwUrl, runtime) {
let path = unhandledIpfsPath(request.url)
- path = fixupDnslinkPath(path) // /ipfs/example.com → /ipns/example.com
+
+ // Check if ipfs:// is being used with a domain name
+ if (path && path.startsWith('/ipfs/')) {
+ const extractedPath = path.replace(/^\/ipfs\//, '')
+
+ // If it's a domain name, redirect to error page
+ if (isDomainName(extractedPath)) {
+ // Using the runtime.browser that was passed in from createRequestModifier
+ const errorPageUrl = runtime.browser.runtime.getURL(
+ `dist/landing-pages/protocol-error.html?url=${encodeURIComponent('ipfs://' + extractedPath)}`
+)
+
+ return handleRedirection({
+ originUrl: request.url,
+ redirectUrl: errorPageUrl,
+ request
+ })
+ }
+ }
+
+ path = fixupDnslinkPath(path)
if (isIPFS.path(path)) {
- // replace search query with a request to a public gateway
- // (will be redirected later, if needed)
return handleRedirection({
originUrl: request.url,
redirectUrl: pathAtHttpGateway(path, pubGwUrl),
request
-
})
}
+
+ return null
}
// RECOVERY OF FAILED REQUESTS
@@ -704,11 +739,11 @@ function isRecoverableViaEthDNS (request, state) {
// We can't redirect in onErrorOccurred/onCompleted
// Indead, we recover by opening URL in a new tab that replaces the failed one
// TODO: display an user-friendly prompt when the very first recovery is done
-async function updateTabWithURL (request, redirectUrl, browser) {
+async function updateTabWithURL (request, redirectUrl, browserAPI) {
// Do nothing if the URL remains the same
if (request.url === redirectUrl) return
- return browser.tabs.update(request.tabId, {
+ return browserAPI.tabs.update(request.tabId, {
active: true,
url: redirectUrl
})
diff --git a/package-lock.json b/package-lock.json
index 763d1825f..f5461eb1d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -52,6 +52,7 @@
"bufferutil": "4.0.9",
"c8": "7.12.0",
"chai": "4.3.7",
+ "copy-webpack-plugin": "^13.0.1",
"cross-env": "7.0.3",
"css-loader": "6.7.2",
"download-cli": "1.1.1",
@@ -6610,6 +6611,110 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/copy-webpack-plugin": {
+ "version": "13.0.1",
+ "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.1.tgz",
+ "integrity": "sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob-parent": "^6.0.1",
+ "normalize-path": "^3.0.0",
+ "schema-utils": "^4.2.0",
+ "serialize-javascript": "^6.0.2",
+ "tinyglobby": "^0.2.12"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3"
+ },
+ "peerDependencies": {
+ "ajv": "^8.8.2"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/copy-webpack-plugin/node_modules/schema-utils": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
+ "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.9",
+ "ajv": "^8.9.0",
+ "ajv-formats": "^2.1.1",
+ "ajv-keywords": "^5.1.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/serialize-javascript": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
"node_modules/core-js": {
"version": "3.29.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.29.0.tgz",
@@ -19251,6 +19356,54 @@
"node": ">=0.10.0"
}
},
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
diff --git a/package.json b/package.json
index 244142388..faeddc064 100644
--- a/package.json
+++ b/package.json
@@ -103,6 +103,7 @@
"bufferutil": "4.0.9",
"c8": "7.12.0",
"chai": "4.3.7",
+ "copy-webpack-plugin": "^13.0.1",
"cross-env": "7.0.3",
"css-loader": "6.7.2",
"download-cli": "1.1.1",
diff --git a/webpack.config.js b/webpack.config.js
index 18cb4d71d..78a428f77 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -5,6 +5,7 @@ import TerserPlugin from 'terser-webpack-plugin'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import { fileURLToPath } from 'url'
import { createRequire } from 'module'
+import CopyWebpackPlugin from 'copy-webpack-plugin'
const require = createRequire(import.meta.url)
const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -57,6 +58,11 @@ const commonConfig = {
IPFS_MONITORING: false,
DEBUG: false // controls verbosity of Hapi HTTP server in js-ipfs
}
+ }),
+ new CopyWebpackPlugin({
+ patterns: [
+ { from: 'add-on/src/landing-pages/protocol-error', to: '../landing-pages' }
+ ]
})
],
module: {