Skip to content

Instantly share code, notes, and snippets.

@filips123
Last active September 2, 2019 13:05
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save filips123/14d5979db24b20cdcfd40e3faacf1033 to your computer and use it in GitHub Desktop.
Save filips123/14d5979db24b20cdcfd40e3faacf1033 to your computer and use it in GitHub Desktop.
ZeroFrame WebSocket API for JavaScript

ZeroFrame WebSocket API for JavaScript

About

This is JavaScript WebSocket client for ZeroFrame API. It supports (almost) same features as default ZeroFrame that is included in ZeroNet sites, but it is using WebSocket client so it can be used in local programs, such as Node.js and Electron.

This is part of clients that will try to add ZeroNet support to local programs outside the browser. I'm currently also creating clients for Python and PHP which will add more possibilities for ZeroNet usage. Please let me know in comments which languages would you also like to support.

The program is now also available as GitHub repository and NPM package on filips123/ZeroFrameJS. Updated documentation is also available there.

This program is currently only available as GitHub Gist, but I will soon publish it to GitHub repository and NPM. I will also properly document it and add some more features.

It is currently not intended for usage, but only for testing. There is also a chance that client API will change until it will be stable. When it will be ready, I will publish it.

Usage

This client is only available in Node.js or another local runtime because it can't access WebSocket in browsers because of CORS restrictions.

To create a connection, you need to specify the ZeroNet site address:

const zeroframe = ZeroFrame('1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D')

If ZeroNet instance is using Multiuser plugin (only local mode is supported), you need to specify a master address of the account you want to use:

const zeroframe = ZeroFrame('1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D', '1Hxki73XprDRedUdA3Remm3kBX5FZxhFR3')

If needed, you can also specify host and port of ZeroNet instance:

const zeroframe = ZeroFrame('1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D', null, '192.168.1.1`, 8080)

If ZeroNet instance is using SSL, you can also specify to use it:

const zeroframe = ZeroFrame('1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D', null, '0net.io`, 443, true)

By default, the client will try to reconnect WebSocket if the connection was closed every 10 seconds. If you don't want this, you can disallow reconnect. In this case, the client will throw an error on a closed connection:

const zeroframe = ZeroFrame('1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D', null, '127.0.0.1', 80, false, true)

The client will then obtain wrapper key to the site and connect to WebSocket using it.

You can now normally use ZeroFrame API. Just remember that there is no wrapper, so wrapper commands are not available. The client is connected directly to the WebSocket server, so you need to use its commands.

Note that the WebSocket server sometimes sends commands (notification, progress, error, prompt, confirm, setSiteInfo, setAnnouncerInfo, updating, redirect, injectHtml, injectScript) that are normally handled by the wrapper. Because there is no wrapper, you need to handle those commands yourself if needed. Commands response and ping are already handled by this client so you don't need to handle them.

You can use the cmd method to issue commands:

zeroframe.cmd(
  'siteInfo',
  {},
  (result) => {
    console.log(result)
  }
)

You can also use the cmdp method to get results as JavaScript promises:

let result = await zeroframe.cmd('siteInfo', {})

To submit responses, you need to use response command:

zeroframe.response(10, 'Hello World')

There are also log and error methods which are available for logging:

zeroframe.log('Connected')
zeroframe.error('Connection failed')

There are also public handler methods which you can overwrite to add your own logic to ZeroFrame:

class ZeroApp extends ZeroFrame {
  onRequest (cmd, message) {
    if (cmd === 'helloWorld') {
      this.log('Hello World')
    }
  }

  onOpenWebsocket (event) {
    this.log('Connected to WebSocket')
  }

  onErrorWebsocket (event) {
    this.error('WebSocket connection error')
  }

  onCloseWebsocket (event) {
    this.error('WebSocket connection closed')
  }
}

Discussion

There are a few things I want to discuss before I will publish this as a package and create clients for other languages:

  • Which languages should be supported? I'm planning to also add support for Python and PHP. Please let me know which languages would you also like to support.

  • Multiuser support only works if the user already exists. Login with a master address is only supported if the user already exists in users.json. To add more usage possibilities, there could also be a support to login directly with master address and private key and to add user automatically to users.json. This would also add the possibility to use this client with an instance that uses a Multiuser plugin that is not in local mode.

  • There should be better error handling. The current version doesn't have so great error handling and logging. Many things are just written to stdout and stderr which can lead to the bloated console. The only error that is thrown is when a connection is closed and reconnect is disabled. There should be better error handling and logging. For logging, the program should use some good log management package.

  • Constructor has many arguments. Although it is enough for most cases to just specify site address, developers may also want to change other constructor arguments. In that case, a constructor call will become bloated with many arguments which could still have a default value. For example, to change allowReconnect to false, all other arguments need to be typed before it. Solution for this will be to only add site address as a positional argument and other arguments as an options object. With this, optional arguments will be able to be set as keywords, similarly as Python does.

  • There needs to be standard API between all languages. All clients in all languages need to have almost the same API. This will allow an easier transition from one language to another. Some exceptions to this are arguments in the constructor (see above) because in Python they can be natively used as keyword arguments, and cmdp method because some languages don't have promises as JavaScript does.

  • Wrapper is not available. Because this client is a direct connection to WebSocket, wrapper and its commands are not available. Developers would need to implement required commands (listed in usage section) themselves. Also, some commands output HTML or JavaScript that was intended to be included in the website. Because there is no website, they would need to be implemented in the other way. More advanced solution for this would be to create a complete ZeroNet developing framework for console applications. Something like an alternative to current web UI and wrapper, which would be written in Python. I got this idea from Shadowlands (about it) which is almost the same thing but for Ethereum (to use Ethereum dapps without web). This is currently not in my plan, but it could be considered in the future.

{
"dependencies": {
"cross-fetch": "^3.0.4",
"ws": "^7.0.1"
}
}
'use strict'
const WebSocket = require('ws')
const fetch = require('cross-fetch')
const CMD_RESPONSE = 'response'
const CMD_PING = 'ping'
const CMD_PONG = 'pong'
class ZeroFrame {
// Initialization & Connection
constructor (site, masterAddress = null, host = '127.0.0.1', port = 43110, secure = false, allowReconnect = true) {
if (!site) {
throw new Error('Site address is not specified')
}
this.masterAddress = masterAddress
this.allowReconnect = allowReconnect
this.site = site
this.host = host
this.port = port
this.secure = secure
this.url = 'http' + (this.secure ? 's' : '') + '://' + this.host + ':' + this.port + '/' + this.site
this.websocketConnected = false
this.waitingCallbacks = {}
this.waitingMessages = []
this.nextMessageId = 1
this._connect()
}
init () {
return this
}
async _connect () {
this.wrapperKey = await this._getWrapperKey()
this.websocket = await this._getWebsocket()
return this.init()
}
async _getWrapperKey () {
let wrapperRequest = await fetch(this.url, { headers: { 'Accept': 'text/html' } })
let wrapperBody = await wrapperRequest.text()
let wrapperKey = wrapperBody.match(/wrapper_key = "(.*?)"/)[1]
return wrapperKey
}
async _getWebsocket () {
let wsUrl = 'ws' + (this.secure ? 's' : '') + '://' + this.host + ':' + this.port + '/Websocket?wrapper_key=' + this.wrapperKey
let wsClient
if (!this.masterAddress) {
wsClient = new WebSocket(wsUrl)
} else {
wsClient = new WebSocket(
wsUrl,
[],
{
headers: {
Cookie: 'master_address=' + this.masterAddress
}
}
)
}
wsClient.onmessage = this._onMessage.bind(this)
wsClient.onopen = this._onOpenWebsocket.bind(this)
wsClient.onerror = this._onErrorWebsocket.bind(this)
wsClient.onclose = this._onCloseWebsocket.bind(this)
return wsClient
}
// Internal handlers
_onMessage (event) {
let message = JSON.parse(event.data)
let cmd = message.cmd
if (cmd === CMD_RESPONSE) {
if (this.waitingCallbacks[message.to] !== undefined) {
this.waitingCallbacks[message.to](message.result)
} else {
this.error('Websocket callback not found:', message)
}
} else if (cmd === CMD_PING) {
this.response(message.id, CMD_PONG)
} else {
this.onRequest(cmd, message)
}
}
_onOpenWebsocket (event) {
this.websocketConnected = true
this.waitingMessages.forEach((message) => {
if (!message.processed) {
this.websocket.send(JSON.stringify(message))
message.processed = true
}
})
this.onOpenWebsocket(event)
}
_onErrorWebsocket (event) {
this.onErrorWebsocket(event)
}
_onCloseWebsocket (event) {
this.websocketConnected = false
this.onCloseWebsocket(event)
if (!this.allowReconnect) {
throw new Error('Connection closed')
}
this.error('Connection closed - Trying to reconnect')
setTimeout(async () => {
this.websocket = await this._getWebsocket()
}, 10000)
}
// External handlers
onRequest (cmd, message) {
this.log('Unknown request', message)
}
onOpenWebsocket (event) {
this.log('Websocket open')
}
onErrorWebsocket (event) {
this.error('Websocket open')
}
onCloseWebsocket (event) {
this.log('Websocket close')
}
// Logging functions
log (...args) {
console.log.apply(console, ['[ZeroFrame]'].concat(args))
}
error (...args) {
console.error.apply(console, ['[ZeroFrame]'].concat(args))
}
// Command functions
_send (message, cb = null) {
if (!message.id) {
message.id = this.nextMessageId
this.nextMessageId++
}
if (this.websocketConnected) {
this.websocket.send(JSON.stringify(message))
} else {
this.waitingMessages.push(message)
}
if (cb) {
this.waitingCallbacks[message.id] = cb
}
}
cmd (cmd, params = {}, cb = null) {
this._send({
cmd: cmd,
params: params
}, cb)
}
cmdp (cmd, params = {}) {
return new Promise((resolve, reject) => {
this.cmd(cmd, params, (response) => {
if (response && response.error) {
reject(response.error)
} else {
resolve(response)
}
})
})
}
response (to, result) {
this._send({
cmd: CMD_RESPONSE,
to: to,
result: result
})
}
}
module.exports = ZeroFrame
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment