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
+ +
+ +
+ + +
+ +
+ + Learn more about IPFS addressing → + +
+
+ + + + 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: {