commit
be9ab955c1
@ -1,3 +1,4 @@ |
||||
gist_token=<token> |
||||
account_passphrase=<passphrase> |
||||
account_password=<password> |
||||
account_password=<password> |
||||
NODE_OPTIONS=--max-old-space-size=2048 |
@ -1,7 +1,8 @@ |
||||
{ |
||||
"extends": "../../.eslintrc", |
||||
"rules": { |
||||
"@typescript-eslint/no-explicit-any": "off" |
||||
"@typescript-eslint/no-explicit-any": "off", |
||||
"@typescript-eslint/prefer-namespace-keyword": "off" |
||||
}, |
||||
"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