From 03b9b82a6fbecba4658cc68d43523b9e862da298 Mon Sep 17 00:00:00 2001 From: Esteban Abaroa Date: Mon, 11 Dec 2023 00:49:36 +0000 Subject: [PATCH] feat(electron): add plebbit rpc --- .github/workflows/release.yml | 4 +- ...ctBuild.js => after-all-artifact-build.js} | 0 electron/{beforePack.js => before-pack.js} | 2 + electron/{buildDocker.sh => build-docker.sh} | 2 +- electron/download-ipfs.js | 2 + electron/downloadIpfs.js | 2 - electron/log.js | 2 +- electron/main.js | 32 ++++++--- electron/preload.js | 49 +++---------- electron/{proxyServer.js => proxy-server.js} | 0 electron/{startIpfs.js => start-ipfs.js} | 6 +- electron/start-plebbit-rpc.js | 69 +++++++++++++++++++ package.json | 3 +- yarn.lock | 31 ++++++++- 14 files changed, 143 insertions(+), 61 deletions(-) rename electron/{afterAllArtifactBuild.js => after-all-artifact-build.js} (100%) rename electron/{beforePack.js => before-pack.js} (98%) rename electron/{buildDocker.sh => build-docker.sh} (92%) create mode 100644 electron/download-ipfs.js delete mode 100644 electron/downloadIpfs.js rename electron/{proxyServer.js => proxy-server.js} (100%) rename electron/{startIpfs.js => start-ipfs.js} (94%) create mode 100644 electron/start-plebbit-rpc.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4f4311fb..41980725 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: node-version: 16 - run: yarn install --frozen-lockfile # make sure the ipfs executable is executable - - run: node electron/downloadIpfs && sudo chmod +x bin/linux/ipfs + - run: node electron/download-ipfs && sudo chmod +x bin/linux/ipfs - run: CI='' yarn build - run: yarn electron:build:linux - run: ls dist @@ -54,7 +54,7 @@ jobs: node-version: 16 - run: yarn install --frozen-lockfile # make sure the ipfs executable is executable - - run: node electron/downloadIpfs && sudo chmod +x bin/mac/ipfs + - run: node electron/download-ipfs && sudo chmod +x bin/mac/ipfs - run: CI='' yarn build - run: yarn electron:build:mac - run: ls dist diff --git a/electron/afterAllArtifactBuild.js b/electron/after-all-artifact-build.js similarity index 100% rename from electron/afterAllArtifactBuild.js rename to electron/after-all-artifact-build.js diff --git a/electron/beforePack.js b/electron/before-pack.js similarity index 98% rename from electron/beforePack.js rename to electron/before-pack.js index 1854e6d9..2495df58 100644 --- a/electron/beforePack.js +++ b/electron/before-pack.js @@ -83,6 +83,8 @@ const downloadIpfsClients = async () => { await download(ipfsClientLinuxPUrl, ipfsClientLinuxPath); }; +exports.downloadIpfsClients = downloadIpfsClients + exports.default = async (context) => { await downloadIpfsClients(); }; diff --git a/electron/buildDocker.sh b/electron/build-docker.sh similarity index 92% rename from electron/buildDocker.sh rename to electron/build-docker.sh index da8c8068..a051f1ed 100755 --- a/electron/buildDocker.sh +++ b/electron/build-docker.sh @@ -8,7 +8,7 @@ then fi # download ipfs clients -node electron/downloadIpfs || { echo "Error: failed script 'node electron/downloadIpfs'" ; exit 1; } +node electron/download-ipfs || { echo "Error: failed script 'node electron/download-ipfs'" ; exit 1; } dockerfile=' FROM electronuserland/builder:16 diff --git a/electron/download-ipfs.js b/electron/download-ipfs.js new file mode 100644 index 00000000..e8f6ba25 --- /dev/null +++ b/electron/download-ipfs.js @@ -0,0 +1,2 @@ +const downloadIpfsClients = require('./before-pack').downloadIpfsClients; +downloadIpfsClients(); diff --git a/electron/downloadIpfs.js b/electron/downloadIpfs.js deleted file mode 100644 index 901d5b7c..00000000 --- a/electron/downloadIpfs.js +++ /dev/null @@ -1,2 +0,0 @@ -const downloadIpfsClients = require('./beforePack').default; -downloadIpfsClients(); diff --git a/electron/log.js b/electron/log.js index e0485154..ea2d19f1 100644 --- a/electron/log.js +++ b/electron/log.js @@ -1,6 +1,6 @@ // require this file to log to file in case there's a crash -const envPaths = require('env-paths').default('plebchan', { suffix: false }); +const envPaths = require('env-paths').default('plebbit', { suffix: false }); const util = require('util'); const fs = require('fs-extra'); const path = require('path'); diff --git a/electron/main.js b/electron/main.js index ad7b78ea..d858db73 100644 --- a/electron/main.js +++ b/electron/main.js @@ -7,18 +7,32 @@ const { Tray, screen: electronScreen, shell, - dialog, + dialog } = require('electron') const isDev = require('electron-is-dev') const path = require('path') -const startIpfs = require('./startIpfs') +const startIpfs = require('./start-ipfs') +const startPlebbitRpcServer = require('./start-plebbit-rpc') const { URL } = require('node:url') +const tcpPortUsed = require('tcp-port-used') +// retry starting ipfs every 10 second, +// in case it was started by another client that shut down and shut down ipfs with it let startIpfsError -startIpfs().catch((e) => { - startIpfsError = e - console.error(e) -}) +setInterval(async () => { + try { + const started = await tcpPortUsed.check(5001, '127.0.0.1') + if (started) { + return + } + await startIpfs() + } + catch (e) { + console.log(e) + startIpfsError = e + dialog.showErrorBox('IPFS error', startIpfsError.message) + } +}, 10000) // use common user agent instead of electron so img, video, audio, iframe elements don't get blocked // https://www.whatismybrowser.com/guides/the-latest-version/chrome @@ -69,7 +83,7 @@ const createMainWindow = () => { width: 1000, height: 600, show: false, - backgroundColor: 'white', + backgroundColor: '#181818', webPreferences: { webSecurity: true, // must be true or iframe embeds like youtube can do remote code execution nodeIntegration: false, @@ -84,7 +98,7 @@ const createMainWindow = () => { // set custom user agent and other headers for window.fetch requests to prevent origin errors mainWindow.webContents.session.webRequest.onBeforeSendHeaders({urls: ['*://*/*']}, (details, callback) => { - const isIframe = details.frame.parent !== null + const isIframe = !!details.frame?.parent // if not a fetch request (or fetch request is from within iframe), do nothing, filtering webRequest by types doesn't seem to work if (details.resourceType !== 'xhr' || isIframe) { return callback({requestHeaders: details.requestHeaders}) @@ -104,7 +118,7 @@ const createMainWindow = () => { // fix cors errors for window.fetch. must not be enabled for iframe or can cause remote code execution mainWindow.webContents.session.webRequest.onHeadersReceived({urls: ['*://*/*']}, (details, callback) => { - const isIframe = details.frame.parent !== null + const isIframe = !!details.frame?.parent // if not a fetch request (or fetch request is from within iframe), do nothing, filtering webRequest by types doesn't seem to work if (details.resourceType !== 'xhr' || isIframe) { return callback({responseHeaders: details.responseHeaders}) diff --git a/electron/preload.js b/electron/preload.js index 92f23223..56ec5cac 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -1,49 +1,16 @@ -// force IpfsHttpClient to use node-fetch to fix max 6 connections -// temporary, eventually we will fork ipfs-http-client and replace -// 'native-fetch' -const mock = require('mock-require'); -const fetch = require('node-fetch'); -mock('ipfs-utils/src/env.js', { - isTest: false, - isElectron: false, - isElectronMain: false, - isElectronRenderer: false, - isNode: true, - isBrowser: false, - isWebWorker: false, - isEnvWithDom: false, - isReactNative: false, -}); -mock('native-fetch', { - default: fetch.default, - Headers: fetch.Headers, - Request: fetch.Request, - Response: fetch.Response, -}); - -const envPaths = require('env-paths').default('plebchan', { suffix: false }); const { contextBridge } = require('electron') -const path = require('path') // dev uses http://localhost, prod uses file://...index.html -const isDev = window.location.protocol === 'http:'; +const isDev = window.location.protocol === 'http:' const defaultPlebbitOptions = { - // find the user's OS data path - dataPath: !isDev ? envPaths.data : path.join(__dirname, '..', '.plebbit'), - ipfsHttpClientsOptions: ['http://localhost:5001/api/v0'] || undefined, - // TODO: having to define pubsubHttpClientsOptions and ipfsHttpClientsOptions is a bug with plebbit-js - pubsubHttpClientsOptions: ['http://localhost:5001/api/v0'] || undefined, - // electron starts the local ipfs gateway on port 11028 because 8080 is too common - ipfsGatewayUrls: ['http://localhost:11028'] || undefined, -}; + plebbitRpcClientsOptions: ['ws://localhost:9138'] +} -// expose a flag to indicate that we are running in electron -contextBridge.exposeInMainWorld('electron', { isElectron: true }); - -// expose plebbit-js native functions into electron's renderer -contextBridge.exposeInMainWorld('plebbitJsNativeFunctions', require('@plebbit/plebbit-js').nativeFunctions.node) contextBridge.exposeInMainWorld('defaultPlebbitOptions', defaultPlebbitOptions) -// uncomment to log -// localStorage.debug = 'plebbit-js:*,plebbit-react-hooks:*,plebchan:*'; +// expose a flag to indicate that we are running in electron +contextBridge.exposeInMainWorld('electron', { isElectron: true }) + +// uncomment for logs +// localStorage.debug = 'plebbit-js:*,plebbit-react-hooks:*,plebchan:*' diff --git a/electron/proxyServer.js b/electron/proxy-server.js similarity index 100% rename from electron/proxyServer.js rename to electron/proxy-server.js diff --git a/electron/startIpfs.js b/electron/start-ipfs.js similarity index 94% rename from electron/startIpfs.js rename to electron/start-ipfs.js index e2753359..29e894a2 100644 --- a/electron/startIpfs.js +++ b/electron/start-ipfs.js @@ -2,9 +2,9 @@ const isDev = require('electron-is-dev'); const path = require('path'); const { spawn } = require('child_process'); const fs = require('fs-extra'); -const envPaths = require('env-paths').default('plebchan', { suffix: false }); +const envPaths = require('env-paths').default('plebbit', { suffix: false }); const ps = require('node:process'); -const proxyServer = require('./proxyServer'); +const proxyServer = require('./proxy-server'); // use this custom function instead of spawnSync for better logging // also spawnSync might have been causing crash on start on windows @@ -59,7 +59,7 @@ const startIpfs = async () => { } catch (e) {} // dont use 8080 port because it's too common - await spawnAsync(ipfsPath, ['config', 'Addresses.Gateway', '/ip4/127.0.0.1/tcp/11028'], { + await spawnAsync(ipfsPath, ['config', '--json', 'Addresses.Gateway', "null"], { env, hideWindows: true, }); diff --git a/electron/start-plebbit-rpc.js b/electron/start-plebbit-rpc.js new file mode 100644 index 00000000..d0e2189f --- /dev/null +++ b/electron/start-plebbit-rpc.js @@ -0,0 +1,69 @@ +const tcpPortUsed = require('tcp-port-used') +const {PlebbitWsServer} = require('@plebbit/plebbit-js/rpc') +const path = require('path') +const envPaths = require('env-paths').default('plebbit', { suffix: false }) +const {randomBytes} = require('crypto') +const fs = require('fs-extra') + +let isDev = true +try { + isDev = require('electron-is-dev') +} catch (e) {} + +// PLEB, always run plebbit rpc on this port so all clients can use it +const port = 9138 +const defaultPlebbitOptions = { + // find the user's OS data path + dataPath: !isDev ? envPaths.data : path.join(__dirname, '..', '.plebbit'), + ipfsHttpClientsOptions: ['http://localhost:5001/api/v0'], + // TODO: having to define pubsubHttpClientsOptions and ipfsHttpClientsOptions is a bug with plebbit-js + pubsubHttpClientsOptions: ['http://localhost:5001/api/v0'], +} + +// generate plebbit rpc auth key if doesn't exist +const plebbitRpcAuthKeyPath = path.join(defaultPlebbitOptions.dataPath, 'auth-key') +let plebbitRpcAuthKey +try { + plebbitRpcAuthKey = fs.readFileSync(plebbitRpcAuthKeyPath, 'utf8') +} +catch (e) { + plebbitRpcAuthKey = randomBytes(32).toString('base64').replace(/[/+=]/g, '').substring(0, 40) + fs.ensureFileSync(plebbitRpcAuthKeyPath) + fs.writeFileSync(plebbitRpcAuthKeyPath, plebbitRpcAuthKey) +} + +let pendingStart = false +const start = async () => { + if (pendingStart) { + return + } + pendingStart = true + try { + const started = await tcpPortUsed.check(port, '127.0.0.1') + if (started) { + return + } + const plebbitWebSocketServer = await PlebbitWsServer({port, plebbitOptions: defaultPlebbitOptions, authKey: plebbitRpcAuthKey}) + + console.log(`plebbit rpc: listening on ws://localhost:${port} (local connections only)`) + console.log(`plebbit rpc: listening on ws://localhost:${port}/${plebbitRpcAuthKey} (secret auth key for remote connections)`) + plebbitWebSocketServer.ws.on('connection', (socket, request) => { + console.log('plebbit rpc: new connection') + // debug raw JSON RPC messages in console + if (isDev) { + socket.on('message', (message) => console.log(`plebbit rpc: ${message.toString()}`)) + } + }) + } + catch (e) { + console.log('failed starting plebbit rpc server', e) + } + pendingStart = false +} + +// retry starting the plebbit rpc server every 1 second, +// in case it was started by another client that shut down and shut down the server with it +start() +setInterval(() => { + start() +}, 1000) diff --git a/package.json b/package.json index 34c470db..cce1bbb1 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "remark-breaks": "3.0.2", "styled-components": "5.3.9", "web-vitals": "3.3.0", + "tcp-port-used": "1.0.2", "zustand": "4.3.6" }, "scripts": { @@ -61,7 +62,7 @@ "electron:build:windows": "electron-builder build --publish never -w", "electron:build:mac": "electron-builder build --publish never -m", "electron:before": "yarn electron:before:download-ipfs && yarn electron:before:delete-data", - "electron:before:download-ipfs": "node electron/downloadIpfs", + "electron:before:download-ipfs": "node electron/download-ipfs", "electron:before:delete-data": "rimraf .plebbit", "android:build:icons": "cordova-res android --skip-config --copy --resources /tmp/plebbit-react-android-icons --icon-source ./android/icons/icon.png --splash-source ./android/icons/splash.png --icon-foreground-source ./android/icons/icon-foreground.png --icon-background-source '#ffffee'", "prettier": "prettier src/**/*.{js,jsx} --write", diff --git a/yarn.lock b/yarn.lock index 40ac5a6a..f027eb2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6345,6 +6345,13 @@ debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, de dependencies: ms "2.1.2" +debug@4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + dependencies: + ms "2.1.2" + debug@4.3.3: version "4.3.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" @@ -9069,7 +9076,7 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -ip-regex@^4.0.0: +ip-regex@^4.0.0, ip-regex@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== @@ -9578,6 +9585,11 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-url@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" + integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== + is-utf8@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -9620,6 +9632,15 @@ is-yarn-global@^0.3.0: resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== +is2@^2.0.6: + version "2.0.9" + resolved "https://registry.yarnpkg.com/is2/-/is2-2.0.9.tgz#ff63b441f90de343fa8fac2125ee170da8e8240d" + integrity sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g== + dependencies: + deep-is "^0.1.3" + ip-regex "^4.1.0" + is-url "^1.2.4" + isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" @@ -15101,6 +15122,14 @@ tarn@^3.0.2: resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.2.tgz#73b6140fbb881b71559c4f8bfde3d9a4b3d27693" integrity sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ== +tcp-port-used@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tcp-port-used/-/tcp-port-used-1.0.2.tgz#9652b7436eb1f4cfae111c79b558a25769f6faea" + integrity sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA== + dependencies: + debug "4.3.1" + is2 "^2.0.6" + temp-dir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e"