commit
be9ab955c1
@ -1,3 +1,4 @@ |
|||||||
gist_token=<token> |
gist_token=<token> |
||||||
account_passphrase=<passphrase> |
account_passphrase=<passphrase> |
||||||
account_password=<password> |
account_password=<password> |
||||||
|
NODE_OPTIONS=--max-old-space-size=2048 |
@ -1,7 +1,8 @@ |
|||||||
{ |
{ |
||||||
"extends": "../../.eslintrc", |
"extends": "../../.eslintrc", |
||||||
"rules": { |
"rules": { |
||||||
"@typescript-eslint/no-explicit-any": "off" |
"@typescript-eslint/no-explicit-any": "off", |
||||||
|
"@typescript-eslint/prefer-namespace-keyword": "off" |
||||||
}, |
}, |
||||||
"ignorePatterns": ["!**/*"] |
"ignorePatterns": ["!**/*"] |
||||||
} |
} |
||||||
|
@ -1 +0,0 @@ |
|||||||
Subproject commit 1a9ec3230e7a3c278ddc6344e5c89d488a316910 |
|
@ -0,0 +1 @@ |
|||||||
|
{ "extends": "../../.eslintrc", "rules": {}, "ignorePatterns": ["!**/*"] } |
@ -0,0 +1,49 @@ |
|||||||
|
# Remixd |
||||||
|
|
||||||
|
`remixd` is a tool that intend to be used with [Remix IDE](https://github.com/ethereum/remix-project) (aka. Browser-Solidity). It allows a websocket connection between |
||||||
|
`Remix IDE` (web application) and the local computer. |
||||||
|
|
||||||
|
Practically Remix IDE makes available a folder shared by `remixd`. |
||||||
|
|
||||||
|
More details are explained in this [tutorial](https://remix-ide.readthedocs.io/en/latest/remixd.html). |
||||||
|
|
||||||
|
Alternatively `remixd` can be used to setup a development environment that can be used with other popular frameworks like Embark, Truffle, Ganache, etc.. |
||||||
|
|
||||||
|
`remixd` needs `npm` and `node` |
||||||
|
|
||||||
|
## INSTALLATION |
||||||
|
|
||||||
|
`npm install -g @remix-project/remixd` |
||||||
|
|
||||||
|
## HELP SECTION |
||||||
|
|
||||||
|
``` |
||||||
|
Usage: remixd -s <shared folder> --remix-ide https://remix.ethereum.org |
||||||
|
|
||||||
|
Provide a two-way connection between the local computer and Remix IDE. |
||||||
|
|
||||||
|
|
||||||
|
Options: |
||||||
|
|
||||||
|
--remix-ide <url> URL of remix instance allowed to connect to this |
||||||
|
web sockect connection |
||||||
|
-s, --shared-folder <path> Folder to share with Remix IDE |
||||||
|
--read-only Treat shared folder as read-only (experimental) |
||||||
|
-h, --help output usage information |
||||||
|
|
||||||
|
``` |
||||||
|
|
||||||
|
## SHARE A FOLDER |
||||||
|
|
||||||
|
`remixd -s <absolute-path> --remix-ide https://remix.ethereum.org` |
||||||
|
|
||||||
|
The current user should have `read/write` access to the folder (at least `read` access). |
||||||
|
|
||||||
|
It is important to notice that changes made to the current file in `Remix IDE` are automatically saved to the local computer every 5000 ms. There is no `Save` action. But the `Ctrl-Z` (undo) can be used. |
||||||
|
|
||||||
|
Furthermore : |
||||||
|
- No copy of the shared folder are kept in the browser storage. |
||||||
|
- It is not possible to create a file from `Remix IDE` (that might change). |
||||||
|
- If a folder does not contain any file, the folder will not be displayed in the explorer (that might change). |
||||||
|
- Symbolic links are not forwarded to Remix IDE. |
||||||
|
|
@ -0,0 +1,15 @@ |
|||||||
|
/* eslint-disable */ |
||||||
|
module.exports = { |
||||||
|
name: 'remixd', |
||||||
|
preset: '../../jest.config.js', |
||||||
|
globals: { |
||||||
|
'ts-jest': { |
||||||
|
tsConfig: '<rootDir>/tsconfig.spec.json' |
||||||
|
} |
||||||
|
}, |
||||||
|
transform: { |
||||||
|
'^.+\\.[tj]sx?$': 'ts-jest' |
||||||
|
}, |
||||||
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], |
||||||
|
coverageDirectory: '../../coverage/libs/remixd' |
||||||
|
}; |
@ -0,0 +1,5 @@ |
|||||||
|
{ |
||||||
|
"watch": ["./src", "./bin"], |
||||||
|
"ext": "ts", |
||||||
|
"exec": "npm run build && npm run start" |
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
{ |
||||||
|
"name": "@remix-project/remixd", |
||||||
|
"version": "0.2.4-alpha.0", |
||||||
|
"description": "remix server: allow accessing file system from remix.ethereum.org and start a dev environment (see help section)", |
||||||
|
"main": "./index.js", |
||||||
|
"types": "./index.d.ts", |
||||||
|
"bin": { |
||||||
|
"remixd": "./bin/remixd.js" |
||||||
|
}, |
||||||
|
"scripts": { |
||||||
|
"test": "echo \"Error: no test specified\"", |
||||||
|
"start": "./bin/remixd.js", |
||||||
|
"npip": "npip", |
||||||
|
"lint": "eslint ./src ./bin --ext .ts", |
||||||
|
"build": "tsc -p ./ && chmod +x ./bin/remixd.js", |
||||||
|
"dev": "nodemon" |
||||||
|
}, |
||||||
|
"repository": { |
||||||
|
"type": "git", |
||||||
|
"url": "git+https://github.com/ethereum/remix-project.git" |
||||||
|
}, |
||||||
|
"keywords": [ |
||||||
|
"remix", |
||||||
|
"ide", |
||||||
|
"ethereum", |
||||||
|
"solidity" |
||||||
|
], |
||||||
|
"author": "Remix Team", |
||||||
|
"license": "MIT", |
||||||
|
"bugs": { |
||||||
|
"url": "https://github.com/ethereum/remix-project/issues" |
||||||
|
}, |
||||||
|
"homepage": "https://github.com/ethereum/remix-project#readme", |
||||||
|
"dependencies": { |
||||||
|
"@remixproject/plugin": "0.3.0-beta.5", |
||||||
|
"@remixproject/plugin-api": "0.3.0-beta.5", |
||||||
|
"@remixproject/plugin-utils": "0.3.0-beta.5", |
||||||
|
"@remixproject/plugin-ws": "^0.3.0-beta.8", |
||||||
|
"axios": "^0.20.0", |
||||||
|
"chokidar": "^2.1.8", |
||||||
|
"commander": "^2.20.3", |
||||||
|
"fs-extra": "^3.0.1", |
||||||
|
"isbinaryfile": "^3.0.2", |
||||||
|
"ws": "^7.3.0" |
||||||
|
}, |
||||||
|
"python": { |
||||||
|
"execPath": "python3", |
||||||
|
"dependencies": { |
||||||
|
"vyper": ">=0.1.0b3" |
||||||
|
} |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@types/axios": "^0.14.0", |
||||||
|
"@types/fs-extra": "^9.0.1", |
||||||
|
"@types/node": "^14.0.5", |
||||||
|
"@types/ws": "^7.2.4", |
||||||
|
"@typescript-eslint/eslint-plugin": "^3.2.0", |
||||||
|
"@typescript-eslint/parser": "^3.2.0", |
||||||
|
"eslint": "6.8.0", |
||||||
|
"eslint-config-standard": "14.1.1", |
||||||
|
"eslint-plugin-import": "2.20.2", |
||||||
|
"eslint-plugin-node": "11.1.0", |
||||||
|
"eslint-plugin-promise": "4.2.1", |
||||||
|
"eslint-plugin-standard": "4.0.1", |
||||||
|
"nodemon": "^2.0.4", |
||||||
|
"ts-node": "^8.10.1", |
||||||
|
"typescript": "^3.9.3" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,99 @@ |
|||||||
|
#!/usr/bin/env node |
||||||
|
import WebSocket from '../websocket' |
||||||
|
import * as servicesList from '../serviceList' |
||||||
|
import * as WS from 'ws' |
||||||
|
import { getDomain } from '../utils' |
||||||
|
import Axios from 'axios' |
||||||
|
import * as fs from 'fs-extra' |
||||||
|
import * as path from 'path' |
||||||
|
import * as program from 'commander' |
||||||
|
|
||||||
|
(async () => { |
||||||
|
program |
||||||
|
.usage('-s <shared folder>') |
||||||
|
.description('Provide a two-way connection between the local computer and Remix IDE') |
||||||
|
.option('--remix-ide <url>', 'URL of remix instance allowed to connect to this web sockect connection') |
||||||
|
.option('-s, --shared-folder <path>', 'Folder to share with Remix IDE') |
||||||
|
.option('--read-only', 'Treat shared folder as read-only (experimental)') |
||||||
|
.on('--help', function(){ |
||||||
|
console.log('\nExample:\n\n remixd -s ./ --remix-ide http://localhost:8080') |
||||||
|
}).parse(process.argv) |
||||||
|
// eslint-disable-next-line
|
||||||
|
const killCallBack: Array<Function> = [] |
||||||
|
|
||||||
|
if (!program.remixIde) { |
||||||
|
console.log('\x1b[33m%s\x1b[0m', '[WARN] You can only connect to remixd from one of the supported origins.') |
||||||
|
} else { |
||||||
|
const isValid = await isValidOrigin(program.remixIde) |
||||||
|
/* Allow unsupported origins and display warning. */ |
||||||
|
if (!isValid) { |
||||||
|
console.log('\x1b[33m%s\x1b[0m', '[WARN] You are using IDE from an unsupported origin.') |
||||||
|
console.log('\x1b[33m%s\x1b[0m', 'Check https://gist.github.com/EthereumRemix/091ccc57986452bbb33f57abfb13d173 for list of all supported origins.\n') |
||||||
|
// return
|
||||||
|
} |
||||||
|
console.log('\x1b[33m%s\x1b[0m', '[WARN] You may now only use IDE at ' + program.remixIde + ' to connect to that instance') |
||||||
|
} |
||||||
|
|
||||||
|
if (program.sharedFolder) { |
||||||
|
console.log('\x1b[33m%s\x1b[0m', '[WARN] Any application that runs on your computer can potentially read from and write to all files in the directory.') |
||||||
|
console.log('\x1b[33m%s\x1b[0m', '[WARN] Symbolic links are not forwarded to Remix IDE\n') |
||||||
|
try { |
||||||
|
const sharedFolderClient = new servicesList['sharedfolder']() |
||||||
|
const websocketHandler = new WebSocket(65520, { remixIdeUrl: program.remixIde }, sharedFolderClient) |
||||||
|
|
||||||
|
websocketHandler.start((ws: WS) => { |
||||||
|
sharedFolderClient.setWebSocket(ws) |
||||||
|
sharedFolderClient.setupNotifications(program.sharedFolder) |
||||||
|
sharedFolderClient.sharedFolder(program.sharedFolder, program.readOnly || false) |
||||||
|
}) |
||||||
|
killCallBack.push(websocketHandler.close.bind(websocketHandler)) |
||||||
|
} catch(error) { |
||||||
|
throw new Error(error) |
||||||
|
} |
||||||
|
} else { |
||||||
|
console.log('\x1b[31m%s\x1b[0m', '[ERR] No valid shared folder provided.') |
||||||
|
} |
||||||
|
|
||||||
|
// kill
|
||||||
|
function kill () { |
||||||
|
for (const k in killCallBack) { |
||||||
|
try { |
||||||
|
killCallBack[k]() |
||||||
|
} catch (e) { |
||||||
|
console.log(e) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
process.on('SIGINT', kill) // catch ctrl-c
|
||||||
|
process.on('SIGTERM', kill) // catch kill
|
||||||
|
process.on('exit', kill) |
||||||
|
|
||||||
|
async function isValidOrigin (origin: string): Promise<any> { |
||||||
|
if (!origin) return false |
||||||
|
const domain = getDomain(origin) |
||||||
|
const gistUrl = 'https://gist.githubusercontent.com/EthereumRemix/091ccc57986452bbb33f57abfb13d173/raw/3367e019335746b73288e3710af2922d4c8ef5a3/origins.json' |
||||||
|
|
||||||
|
try { |
||||||
|
const { data } = await Axios.get(gistUrl) |
||||||
|
|
||||||
|
try { |
||||||
|
await fs.writeJSON(path.resolve(__dirname + '/../origins.json'), { data }) |
||||||
|
} catch (e) { |
||||||
|
console.error(e) |
||||||
|
} |
||||||
|
|
||||||
|
return data.includes(origin) ? data.includes(origin) : data.includes(domain) |
||||||
|
} catch (e) { |
||||||
|
try { |
||||||
|
// eslint-disable-next-line
|
||||||
|
const origins = require('../origins.json') |
||||||
|
const { data } = origins |
||||||
|
|
||||||
|
return data.includes(origin) ? data.includes(origin) : data.includes(domain) |
||||||
|
} catch (e) { |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
})() |
@ -0,0 +1,12 @@ |
|||||||
|
'use strict' |
||||||
|
import { RemixdClient as sharedFolder } from './services/remixdClient' |
||||||
|
import Websocket from './websocket' |
||||||
|
import * as utils from './utils' |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
Websocket, |
||||||
|
utils, |
||||||
|
services: { |
||||||
|
sharedFolder |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
{ |
||||||
|
"data":[ |
||||||
|
"http://remix-alpha.ethereum.org", |
||||||
|
"http://remix.ethereum.org", |
||||||
|
"https://remix-alpha.ethereum.org", |
||||||
|
"https://remix.ethereum.org", |
||||||
|
"package://a7df6d3c223593f3550b35e90d7b0b1f.mod", |
||||||
|
"package://6fd22d6fe5549ad4c4d8fd3ca0b7816b.mod", |
||||||
|
"https://ipfsgw.komputing.org" |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
import { RemixdClient as sharedfolder } from './services/remixdClient' |
||||||
|
|
||||||
|
export { sharedfolder } |
@ -0,0 +1,242 @@ |
|||||||
|
import { PluginClient } from '@remixproject/plugin' |
||||||
|
import { SharedFolderArgs, TrackDownStreamUpdate, Filelist, ResolveDirectory, FileContent } from '../types' |
||||||
|
import * as WS from 'ws' |
||||||
|
import * as utils from '../utils' |
||||||
|
import * as chokidar from 'chokidar' |
||||||
|
import * as fs from 'fs-extra' |
||||||
|
import * as isbinaryfile from 'isbinaryfile' |
||||||
|
|
||||||
|
export class RemixdClient extends PluginClient { |
||||||
|
methods: ['folderIsReadOnly', 'resolveDirectory', 'get', 'exists', 'isFile', 'set', 'list', 'isDirectory'] |
||||||
|
trackDownStreamUpdate: TrackDownStreamUpdate = {} |
||||||
|
websocket: WS |
||||||
|
currentSharedFolder: string |
||||||
|
readOnly: boolean |
||||||
|
|
||||||
|
setWebSocket (websocket: WS): void { |
||||||
|
this.websocket = websocket |
||||||
|
} |
||||||
|
|
||||||
|
sharedFolder (currentSharedFolder: string, readOnly: boolean): void { |
||||||
|
this.currentSharedFolder = currentSharedFolder |
||||||
|
this.readOnly = readOnly |
||||||
|
} |
||||||
|
|
||||||
|
list (): Filelist { |
||||||
|
try { |
||||||
|
return utils.walkSync(this.currentSharedFolder, {}, this.currentSharedFolder) |
||||||
|
} catch (e) { |
||||||
|
throw new Error(e) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
resolveDirectory (args: SharedFolderArgs): ResolveDirectory { |
||||||
|
try { |
||||||
|
const path = utils.absolutePath(args.path, this.currentSharedFolder) |
||||||
|
const result = utils.resolveDirectory(path, this.currentSharedFolder) |
||||||
|
|
||||||
|
return result |
||||||
|
} catch (e) { |
||||||
|
throw new Error(e) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
folderIsReadOnly (): boolean { |
||||||
|
return this.readOnly |
||||||
|
} |
||||||
|
|
||||||
|
get (args: SharedFolderArgs): Promise<FileContent> { |
||||||
|
try { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
const path = utils.absolutePath(args.path, this.currentSharedFolder) |
||||||
|
|
||||||
|
if (!fs.existsSync(path)) { |
||||||
|
return reject('File not found ' + path) |
||||||
|
} |
||||||
|
if (!isRealPath(path)) return |
||||||
|
isbinaryfile(path, (error: Error, isBinary: boolean) => { |
||||||
|
if (error) console.log(error) |
||||||
|
if (isBinary) { |
||||||
|
resolve({ content: '<binary content not displayed>', readonly: true }) |
||||||
|
} else { |
||||||
|
fs.readFile(path, 'utf8', (error: Error, data: string) => { |
||||||
|
if (error) console.log(error) |
||||||
|
resolve({ content: data, readonly: false }) |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} catch (error) { |
||||||
|
throw new Error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
exists (args: SharedFolderArgs): boolean { |
||||||
|
try { |
||||||
|
const path = utils.absolutePath(args.path, this.currentSharedFolder) |
||||||
|
|
||||||
|
return fs.existsSync(path) |
||||||
|
} catch(error) { |
||||||
|
throw new Error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
set (args: SharedFolderArgs): Promise<void> { |
||||||
|
try { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
if (this.readOnly) reject('Cannot write file: read-only mode selected') |
||||||
|
const isFolder = args.path.endsWith('/') |
||||||
|
const path = utils.absolutePath(args.path, this.currentSharedFolder) |
||||||
|
const exists = fs.existsSync(path) |
||||||
|
|
||||||
|
if (exists && !isRealPath(path)) reject() |
||||||
|
if (args.content === 'undefined') { // no !!!!!
|
||||||
|
console.log('trying to write "undefined" ! stopping.') |
||||||
|
reject('trying to write "undefined" ! stopping.') |
||||||
|
} |
||||||
|
this.trackDownStreamUpdate[path] = path |
||||||
|
if (isFolder) { |
||||||
|
fs.mkdirp(path).then(() => { |
||||||
|
let splitPath = args.path.split('/') |
||||||
|
|
||||||
|
splitPath = splitPath.filter(dir => dir) |
||||||
|
const dir = '/' + splitPath.join('/') |
||||||
|
|
||||||
|
this.emit('folderAdded', dir) |
||||||
|
resolve() |
||||||
|
}).catch((e: Error) => reject(e)) |
||||||
|
} else { |
||||||
|
fs.ensureFile(path).then(() => { |
||||||
|
fs.writeFile(path, args.content, 'utf8', (error: Error) => { |
||||||
|
if (error) { |
||||||
|
console.log(error) |
||||||
|
reject(error) |
||||||
|
} |
||||||
|
resolve() |
||||||
|
}) |
||||||
|
}).catch((e: Error) => reject(e)) |
||||||
|
if (!exists) { |
||||||
|
this.emit('fileAdded', args.path) |
||||||
|
} else { |
||||||
|
this.emit('fileChanged', args.path) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} catch (error) { |
||||||
|
throw new Error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
rename (args: SharedFolderArgs): Promise<boolean> { |
||||||
|
try { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
if (this.readOnly) reject('Cannot rename file: read-only mode selected') |
||||||
|
const oldpath = utils.absolutePath(args.oldPath, this.currentSharedFolder) |
||||||
|
|
||||||
|
if (!fs.existsSync(oldpath)) { |
||||||
|
reject('File not found ' + oldpath) |
||||||
|
} |
||||||
|
const newpath = utils.absolutePath(args.newPath, this.currentSharedFolder) |
||||||
|
|
||||||
|
if (!isRealPath(oldpath)) return |
||||||
|
fs.move(oldpath, newpath, (error: Error) => { |
||||||
|
if (error) { |
||||||
|
console.log(error) |
||||||
|
reject(error.message) |
||||||
|
} |
||||||
|
this.emit('fileRenamed', args.oldPath, args.newPath) |
||||||
|
resolve(true) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} catch (error) { |
||||||
|
throw new Error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
remove (args: SharedFolderArgs): Promise<boolean> { |
||||||
|
try { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
if (this.readOnly) reject('Cannot remove file: read-only mode selected') |
||||||
|
const path = utils.absolutePath(args.path, this.currentSharedFolder) |
||||||
|
|
||||||
|
if (!fs.existsSync(path)) reject('File not found ' + path) |
||||||
|
if (!isRealPath(path)) return |
||||||
|
return fs.remove(path, (error: Error) => { |
||||||
|
if (error) { |
||||||
|
console.log(error) |
||||||
|
reject('Failed to remove file/directory: ' + error) |
||||||
|
} |
||||||
|
this.emit('fileRemoved', args.path) |
||||||
|
resolve(true) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} catch (error) { |
||||||
|
throw new Error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
isDirectory (args: SharedFolderArgs): boolean { |
||||||
|
try { |
||||||
|
const path = utils.absolutePath(args.path, this.currentSharedFolder) |
||||||
|
|
||||||
|
return fs.statSync(path).isDirectory() |
||||||
|
} catch (error) { |
||||||
|
throw new Error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
isFile (args: SharedFolderArgs): boolean { |
||||||
|
try { |
||||||
|
const path = utils.absolutePath(args.path, this.currentSharedFolder) |
||||||
|
|
||||||
|
return fs.statSync(path).isFile() |
||||||
|
} catch (error) { |
||||||
|
throw new Error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
setupNotifications (path: string): void { |
||||||
|
const absPath = utils.absolutePath('./', path) |
||||||
|
|
||||||
|
if (!isRealPath(absPath)) return |
||||||
|
const watcher = chokidar.watch(path, { depth: 0, ignorePermissionErrors: true }) |
||||||
|
console.log('setup notifications for ' + path) |
||||||
|
/* we can't listen on created file / folder |
||||||
|
watcher.on('add', (f, stat) => { |
||||||
|
isbinaryfile(f, (error, isBinary) => { |
||||||
|
if (error) console.log(error) |
||||||
|
console.log('add', f) |
||||||
|
this.emit('created', { path: utils.relativePath(f, this.currentSharedFolder), isReadOnly: isBinary, isFolder: false }) |
||||||
|
}) |
||||||
|
}) |
||||||
|
watcher.on('addDir', (f, stat) => { |
||||||
|
this.emit('created', { path: utils.relativePath(f, this.currentSharedFolder), isReadOnly: false, isFolder: true }) |
||||||
|
}) |
||||||
|
*/ |
||||||
|
watcher.on('change', (f: string) => { |
||||||
|
if (this.trackDownStreamUpdate[f]) { |
||||||
|
delete this.trackDownStreamUpdate[f] |
||||||
|
return |
||||||
|
} |
||||||
|
this.emit('changed', utils.relativePath(f, this.currentSharedFolder)) |
||||||
|
}) |
||||||
|
watcher.on('unlink', (f: string) => { |
||||||
|
this.emit('removed', utils.relativePath(f, this.currentSharedFolder), false) |
||||||
|
}) |
||||||
|
watcher.on('unlinkDir', (f: string) => { |
||||||
|
this.emit('removed', utils.relativePath(f, this.currentSharedFolder), true) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function isRealPath (path: string): boolean { |
||||||
|
const realPath = fs.realpathSync(path) |
||||||
|
const isRealPath = path === realPath |
||||||
|
const mes = '[WARN] Symbolic link modification not allowed : ' + path + ' | ' + realPath |
||||||
|
|
||||||
|
if (!isRealPath) { |
||||||
|
console.log('\x1b[33m%s\x1b[0m', mes) |
||||||
|
// throw new Error(mes)
|
||||||
|
} |
||||||
|
return isRealPath |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
import * as ServiceList from '../serviceList' |
||||||
|
import * as Websocket from 'ws' |
||||||
|
|
||||||
|
type ServiceListKeys = keyof typeof ServiceList; |
||||||
|
|
||||||
|
export type SharedFolder = typeof ServiceList[ServiceListKeys] |
||||||
|
|
||||||
|
export type SharedFolderClient = InstanceType<typeof ServiceList[ServiceListKeys]> |
||||||
|
|
||||||
|
export type WebsocketOpt = { |
||||||
|
remixIdeUrl: string |
||||||
|
} |
||||||
|
|
||||||
|
export type FolderArgs = { |
||||||
|
path: string |
||||||
|
} |
||||||
|
|
||||||
|
export type KeyPairString = { |
||||||
|
[key: string]: string |
||||||
|
} |
||||||
|
|
||||||
|
export type ResolveDirectory = { |
||||||
|
[key: string]: { |
||||||
|
isDirectory: boolean |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export type FileContent = { |
||||||
|
content: string |
||||||
|
readonly: boolean |
||||||
|
} |
||||||
|
|
||||||
|
export type TrackDownStreamUpdate = KeyPairString |
||||||
|
|
||||||
|
export type SharedFolderArgs = FolderArgs & KeyPairString |
||||||
|
|
||||||
|
export type WS = typeof Websocket |
||||||
|
|
||||||
|
export type Filelist = KeyPairString |
@ -0,0 +1,89 @@ |
|||||||
|
import { ResolveDirectory, Filelist } from './types' |
||||||
|
import * as fs from 'fs-extra' |
||||||
|
import * as isbinaryfile from 'isbinaryfile' |
||||||
|
import * as pathModule from 'path' |
||||||
|
/** |
||||||
|
* returns the absolute path of the given @arg path |
||||||
|
* |
||||||
|
* @param {String} path - relative path (Unix style which is the one used by Remix IDE) |
||||||
|
* @param {String} sharedFolder - absolute shared path. platform dependent representation. |
||||||
|
* @return {String} platform dependent absolute path (/home/user1/.../... for unix, c:\user\...\... for windows) |
||||||
|
*/ |
||||||
|
function absolutePath (path: string, sharedFolder:string): string { |
||||||
|
path = normalizePath(path) |
||||||
|
if (path.indexOf(sharedFolder) !== 0) { |
||||||
|
path = pathModule.resolve(sharedFolder, path) |
||||||
|
} |
||||||
|
return path |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* return the relative path of the given @arg path |
||||||
|
* |
||||||
|
* @param {String} path - absolute platform dependent path |
||||||
|
* @param {String} sharedFolder - absolute shared path. platform dependent representation |
||||||
|
* @return {String} relative path (Unix style which is the one used by Remix IDE) |
||||||
|
*/ |
||||||
|
function relativePath (path: string, sharedFolder: string): string { |
||||||
|
const relative: string = pathModule.relative(sharedFolder, path) |
||||||
|
|
||||||
|
return normalizePath(relative) |
||||||
|
} |
||||||
|
|
||||||
|
function normalizePath (path: string): string { |
||||||
|
if (process.platform === 'win32') { |
||||||
|
return path.replace(/\\/g, '/') |
||||||
|
} |
||||||
|
return path |
||||||
|
} |
||||||
|
|
||||||
|
function walkSync (dir: string, filelist: Filelist, sharedFolder: string): Filelist { |
||||||
|
const files: string[] = fs.readdirSync(dir) |
||||||
|
|
||||||
|
filelist = filelist || {} |
||||||
|
files.forEach(function (file) { |
||||||
|
const subElement = pathModule.join(dir, file) |
||||||
|
|
||||||
|
if (!fs.lstatSync(subElement).isSymbolicLink()) { |
||||||
|
if (fs.statSync(subElement).isDirectory()) { |
||||||
|
filelist = walkSync(subElement, filelist, sharedFolder) |
||||||
|
} else { |
||||||
|
const relative = relativePath(subElement, sharedFolder) |
||||||
|
|
||||||
|
filelist[relative] = isbinaryfile.sync(subElement) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
return filelist |
||||||
|
} |
||||||
|
|
||||||
|
function resolveDirectory (dir: string, sharedFolder: string): ResolveDirectory { |
||||||
|
const ret: ResolveDirectory = {} |
||||||
|
const files: string[] = fs.readdirSync(dir) |
||||||
|
|
||||||
|
files.forEach(function (file) { |
||||||
|
const subElement = pathModule.join(dir, file) |
||||||
|
|
||||||
|
if (!fs.lstatSync(subElement).isSymbolicLink()) { |
||||||
|
const relative: string = relativePath(subElement, sharedFolder) |
||||||
|
|
||||||
|
ret[relative] = { isDirectory: fs.statSync(subElement).isDirectory() } |
||||||
|
} |
||||||
|
}) |
||||||
|
return ret |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* returns the absolute path of the given @arg url |
||||||
|
* |
||||||
|
* @param {String} url - Remix-IDE URL instance |
||||||
|
* @return {String} extracted domain name from url |
||||||
|
*/ |
||||||
|
function getDomain(url: string) { |
||||||
|
// eslint-disable-next-line
|
||||||
|
const domainMatch = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:\/\n?]+)/img) |
||||||
|
|
||||||
|
return domainMatch ? domainMatch[0] : null |
||||||
|
} |
||||||
|
|
||||||
|
export { absolutePath, relativePath, walkSync, resolveDirectory, getDomain } |
@ -0,0 +1,73 @@ |
|||||||
|
import * as WS from 'ws' |
||||||
|
import * as http from 'http' |
||||||
|
import { WebsocketOpt, SharedFolderClient } from './types' |
||||||
|
import { getDomain } from './utils' |
||||||
|
import { createClient } from '@remixproject/plugin-ws' |
||||||
|
export default class WebSocket { |
||||||
|
server: http.Server |
||||||
|
wsServer: WS.Server |
||||||
|
|
||||||
|
constructor (public port: number, public opt: WebsocketOpt, public sharedFolder: SharedFolderClient) {} |
||||||
|
|
||||||
|
start (callback?: (ws: WS) => void): void { |
||||||
|
this.server = http.createServer((request, response) => { |
||||||
|
console.log((new Date()) + ' Received request for ' + request.url) |
||||||
|
response.writeHead(404) |
||||||
|
response.end() |
||||||
|
}) |
||||||
|
const loopback = '127.0.0.1' |
||||||
|
|
||||||
|
this.server.listen(this.port, loopback, function () { |
||||||
|
console.log((new Date()) + ' remixd is listening on ' + loopback + ':65520') |
||||||
|
}) |
||||||
|
this.wsServer = new WS.Server({ |
||||||
|
server: this.server, |
||||||
|
verifyClient: (info, done) => { |
||||||
|
if (!originIsAllowed(info.origin, this)) { |
||||||
|
done(false) |
||||||
|
console.log((new Date()) + ' Connection from origin ' + info.origin + ' rejected.') |
||||||
|
return |
||||||
|
} |
||||||
|
done(true) |
||||||
|
} |
||||||
|
}) |
||||||
|
this.wsServer.on('connection', (ws) => { |
||||||
|
const { sharedFolder } = this |
||||||
|
|
||||||
|
createClient(ws, sharedFolder as any) |
||||||
|
if(callback) callback(ws) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
close (): void { |
||||||
|
if (this.wsServer) { |
||||||
|
this.wsServer.close(() => { |
||||||
|
this.server.close() |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function originIsAllowed (origin: string, self: WebSocket): boolean { |
||||||
|
if (self.opt.remixIdeUrl) { |
||||||
|
if (self.opt.remixIdeUrl.endsWith('/')) self.opt.remixIdeUrl = self.opt.remixIdeUrl.slice(0, -1) |
||||||
|
return origin === self.opt.remixIdeUrl || origin === getDomain(self.opt.remixIdeUrl) |
||||||
|
} else { |
||||||
|
try { |
||||||
|
// eslint-disable-next-line
|
||||||
|
const origins = require('./origins.json') |
||||||
|
const domain = getDomain(origin) |
||||||
|
const { data } = origins |
||||||
|
|
||||||
|
if (data.includes(origin) || data.includes(domain)) { |
||||||
|
self.opt.remixIdeUrl = origin |
||||||
|
console.log('\x1b[33m%s\x1b[0m', '[WARN] You may now only use IDE at ' + self.opt.remixIdeUrl + ' to connect to that instance') |
||||||
|
return true |
||||||
|
} else { |
||||||
|
return false |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
{ |
||||||
|
"extends": "../../tsconfig.json", |
||||||
|
"files": [], |
||||||
|
"include": [], |
||||||
|
"references": [ |
||||||
|
{ |
||||||
|
"path": "./tsconfig.lib.json" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "./tsconfig.spec.json" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
{ |
||||||
|
"extends": "./tsconfig.json", |
||||||
|
"compilerOptions": { |
||||||
|
"target": "es6", |
||||||
|
"module": "commonjs", |
||||||
|
"outDir": "../../dist/out-tsc", |
||||||
|
"declaration": true, |
||||||
|
"rootDir": "./src", |
||||||
|
"types": ["node"] |
||||||
|
}, |
||||||
|
"exclude": ["**/*.spec.ts"], |
||||||
|
"include": ["bin", "src", "bin/origins.json"] |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
{ |
||||||
|
"extends": "./tsconfig.json", |
||||||
|
"compilerOptions": { |
||||||
|
"outDir": "../../dist/out-tsc", |
||||||
|
"module": "commonjs", |
||||||
|
"types": ["jest", "node"] |
||||||
|
}, |
||||||
|
"include": [ |
||||||
|
"**/*.spec.ts", |
||||||
|
"**/*.spec.tsx", |
||||||
|
"**/*.spec.js", |
||||||
|
"**/*.spec.jsx", |
||||||
|
"**/*.d.ts" |
||||||
|
] |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue