Merge remote-tracking branch 'remixd/master'

remixd
ioedeveloper 4 years ago
commit daedd1db2e
  1. 32
      .circleci/config.yml
  2. 21
      .eslintrc.json
  3. 5
      .gitignore
  4. 11
      .npmignore
  5. 49
      README.md
  6. 11
      bin/origins.json
  7. 97
      bin/remixd.ts
  8. 5
      nodemon.json
  9. 4000
      package-lock.json
  10. 69
      package.json
  11. 12
      src/index.ts
  12. 3
      src/serviceList.ts
  13. 242
      src/services/remixdClient.ts
  14. 91
      src/utils.ts
  15. 76
      src/websocket.ts
  16. 17
      tsconfig.json
  17. 39
      types/index.ts

@ -0,0 +1,32 @@
# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#
version: 2
jobs:
remixd:
docker:
# specify the version you desire here
- image: circleci/node:9.11.2
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
# - image: circleci/mongo:3.4.4
environment:
- COMMIT_AUTHOR_EMAIL: "yann@ethereum.org"
- COMMIT_AUTHOR: "Circle CI"
- FILES_TO_PACKAGE: "package.json"
working_directory: ~/remixd
steps:
- checkout
- run: npm install
- run: npm run lint
- run: npm run test
workflows:
version: 2
build_all:
jobs:
- remixd

@ -0,0 +1,21 @@
{
"env": {
"browser": true,
"commonjs": true,
"es6": true
},
"extends": [
"plugin:@typescript-eslint/recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"rules": {
"standard/no-callback-literal": 0,
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/ban-types": 0
}
}

5
.gitignore vendored

@ -0,0 +1,5 @@
node_modules
npm-debug.log
python_modules
lib
shared

@ -0,0 +1,11 @@
node_modules
npm-debug.log
python_modules
shared
/bin
/src
.circleci
tsconfig.json
nodemon.json
.eslintrc.json
/types

@ -0,0 +1,49 @@
# Remixd
`remixd` is a tool that intend to be used with [Remix IDE](http://github.com/ethereum/browser-solidity) (aka. Browser-Solidity). It allows a websocket connection between
`Remix IDE` (web application) and the local computer.
Practically Remix IDE make available a folder shared by `remixd`.
More details are explained in this [tutorial](http://remix.readthedocs.io/en/latest/tutorial_remixd_filesystem.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 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,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,97 @@
#!/usr/bin/env node
import WebSocket from '../src/websocket'
import * as servicesList from '../src/serviceList'
import { WS } from '../types/index'
import { getDomain } from '../src/utils'
import Axios from 'axios'
import * as fs from 'fs-extra'
import * as path from 'path'
(async () => {
const program = require('commander')
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)
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)
}
}
// 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 {
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,5 @@
{
"watch": ["./src", "./bin"],
"ext": "ts",
"exec": "npm run build && npm run start"
}

4000
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,69 @@
{
"name": "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": "./lib/src/index.js",
"types": "./lib/src/index.d.ts",
"bin": {
"remixd": "./lib/bin/remixd.js"
},
"scripts": {
"test": "echo \"Error: no test specified\"",
"start": "./lib/bin/remixd.js",
"npip": "npip",
"lint": "eslint ./src ./bin --ext .ts",
"build": "tsc -p ./ && chmod +x ./lib/bin/remixd.js",
"dev": "nodemon"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ethereum/remixd.git"
},
"keywords": [
"remix",
"ide",
"ethereum",
"solidity"
],
"author": "cpp ethereum team",
"license": "MIT",
"bugs": {
"url": "https://github.com/ethereum/remixd/issues"
},
"homepage": "https://github.com/ethereum/remixd#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,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,3 @@
import { RemixdClient as sharedfolder } from './services/remixdClient'
export { sharedfolder }

@ -0,0 +1,242 @@
import { PluginClient } from '@remixproject/plugin'
import { SharedFolderArgs, TrackDownStreamUpdate, WS, Filelist, ResolveDirectory, FileContent } from '../../types'
import * as utils from '../utils'
import * as chokidar from 'chokidar'
import * as fs from 'fs-extra'
const isbinaryfile = require('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,91 @@
import { ResolveDirectory, Filelist } from '../types'
const fs = require('fs-extra')
const path = require('path')
const isbinaryfile = require('isbinaryfile')
const pathModule = require('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 = path.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 = path.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) {
const domainMatch = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:\/\n?]+)/img)
return domainMatch ? domainMatch[0] : null
}
export { absolutePath, relativePath, walkSync, resolveDirectory, getDomain }

@ -0,0 +1,76 @@
import * as WS from 'ws'
import * as http from 'http'
import { WebsocketOpt, SharedFolderClient } from '../types'
import { getDomain } from './utils'
const { createClient } = require('@remixproject/plugin-ws')
export default class WebSocket {
server: http.Server
wsServer: WS.Server
connection: WS
constructor (public port: number, public opt: WebsocketOpt, public sharedFolder: SharedFolderClient) {}
start (callback?: Function): 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
this.connection = ws
createClient(ws, sharedFolder)
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 {
const origins = require('../bin/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,17 @@
{
"compilerOptions": {
/* Basic Options */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
"outDir": "./lib",
"strict": true, /* Enable all strict type-checking options. */
"strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["bin", "src", "bin/origins.json"]
}

@ -0,0 +1,39 @@
import * as ServiceList from '../src/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
Loading…
Cancel
Save