Files
nym/clients/webassembly/client.js
T
2020-06-05 18:06:38 +01:00

331 lines
10 KiB
JavaScript

import * as wasm from ".";
import { version } from './package.json';
import { major, minor, parse } from "semver";
/**
* A Nym identity, consisting of a public/private keypair and a Nym
* gateway address.
*/
export class Identity {
// in the future this should allow for loading from local storage
constructor() {
const raw_identity = JSON.parse(wasm.keygen());
this.address = raw_identity.address;
this.privateKey = raw_identity.private_key;
this.publicKey = raw_identity.public_key;
}
}
/**
* A Client which connects to a Nym gateway via websocket. All communication
* with the Nym network happens through this connection.
*/
export class Client {
constructor(directoryUrl, identity, authToken) {
this.authToken = authToken
this.gateway = null; // {socketAddress, mixAddress, conn}
this.identity = identity;
this.topology = null;
this.topologyEndpoint = directoryUrl + "/api/presence/topology";
}
/**
* @return {string} a user-pubkey@nym-gateway recipient address
*/
formatAsRecipient() {
return `${this.identity.address}@${this.gateway.mixAddress}`
}
/**
* Get the current network topology, then connect to this client's Nym gateway
* via websocket.
*/
async start() {
await this.updateTopology();
this._getInitialGateway();
await this.connect();
// TODO: a way to somehow await for our authenticate response to be processed
}
_isRegistered() {
return this.authToken !== null
}
/**
* Checks if given node is compatible with this client
*/
isNodeVersionCompatible(node) {
const clientVersion = parse(version);
const mixVersion = parse(node.version);
return major(clientVersion) === major(mixVersion) && minor(clientVersion) === minor(mixVersion)
}
/**
* Update the Nym network topology.
*
* @returns an object containing the current Nym network topology
*/
async updateTopology() {
let response = await http('get', this.topologyEndpoint);
let topology = JSON.parse(response); // make sure it's a valid json
topology.mixNodes = topology.mixNodes.filter(this.isNodeVersionCompatible)
topology.gatewayNodes = topology.gatewayNodes.filter(this.isNodeVersionCompatible)
this.topology = topology;
this.onUpdatedTopology();
return topology;
}
/**
* Gets the address of a Nym gateway to send the Sphinx packet to.
* At present we choose the first gateway as the network should only be
* running one. Later, we will implement multiple gateways.
*/
_getInitialGateway() {
if (this.gateway !== null) {
console.error("tried to re-initialise gateway data");
return;
}
if (this.topology === null || this.topology.gatewayNodes.length === 0) {
console.error("No gateways available on the network")
}
this.gateway = {
socketAddress: this.topology.gatewayNodes[0].clientListener,
mixAddress: this.topology.gatewayNodes[0].pubKey,
conn: null,
}
}
/**
* Connect to the client's defined Nym gateway via websocket.
*/
connect() {
return new Promise((resolve, reject) => {
const conn = new WebSocket(this.gateway.socketAddress);
conn.onclose = this.onConnectionClose;
conn.onerror = (event) => {
this.onConnectionError(event);
reject(event);
};
conn.onmessage = (event) => this.onMessage(event);
conn.onopen = (event) => {
this.onConnect(event);
if (this._isRegistered()) {
this.sendAuthenticateRequest();
resolve(); // TODO: we should wait for authenticateResponse...
} else {
this.sendRegisterRequest();
resolve(); // TODO: we should wait for registerResponse...
}
}
this.gateway.conn = conn;
})
}
/**
* Sends a registration request to a Nym gateway node. Use it only if you
* haven't registered this client before.
*/
sendRegisterRequest() {
const registerReq = buildRegisterRequest(this.identity.address);
this.gateway.conn.send(registerReq);
this.onRegisterRequestSend();
}
/**
* Authenticates with a Nym gateway for this client.
*
* @param {string} token
*/
sendAuthenticateRequest(token) {
const authenticateReq = buildAuthenticateRequest(this.identity.address, token);
this.conn.send(authenticateReq);
this.onAuthenticateRequestSend();
}
/**
* Sends a message up the websocket to this client's Nym gateway.
*
* NOTE: this currently does not implement chunking and messages over ~1KB
* will cause a panic. This will be fixed in a future version.
*
* `message` must be a {string} at the moment. Binary `Blob` and `ArrayBuffer`
* will be supported soon.
*
* @param {*} message
* @param {string} recipient
*/
sendMessage(message, recipient) {
if (this.gateway === null || this.gateway.conn === null) {
console.error("Client was not initialised");
return
}
if (message instanceof Blob || message instanceof ArrayBuffer) {
// but it wouldn't be difficult to implement it.
console.error("Binary messages are not yet supported");
return
}
const sphinxPacket = wasm.create_sphinx_packet(JSON.stringify(this.topology), message, recipient);
this.gateway.conn.send(sphinxPacket);
this.onMessageSend();
}
/**
* A callback triggered when a message is received from this client's Nym
* gateway.
*
* The `event` may be a binary blob which was the payload of a Sphinx packet,
* or it may be a JSON control message (for example, the result of an
* authenticate request).
* @param {*} event
*/
onMessage(event) {
if (event.data instanceof Blob) {
this.onBlobResponse(event);
} else {
const receivedData = JSON.parse(event.data);
switch (receivedData.type) {
case "send": return this.onSendConfirmation(event);
case "authenticate": return this.onAuthenticateResponse(event);
case "error": return this.onErrorResponse(event);
case "register": return this.onRegisterResponse(event);
default: return this.onUnknownResponse(event);
}
}
}
// all the callbacks that can be overwritten
/**
* A callback that fires when network topology is updated.
*/
onUpdatedTopology() {
console.log("Default: Updated topology")
}
/**
*
* @param {*} event
*/
onConnect(event) {
console.log("Default: Established gateway connection", event);
}
onConnectionClose(event) {
console.log("Default: The the connection to gateway was closed", event);
}
onConnectionError(event) {
console.error("Default: Gateway connection error: ", event);
}
onAuthenticateRequestSend() {
console.log("Default: sent authentication request");
}
onRegisterRequestSend() {
console.log("Default: sent register request");
}
onMessageSend() {
console.log("Default: sent message through gateway to the mixnet");
}
onAuthenticated() {
console.log("Default: we are authenticated");
}
onSendConfirmation(event) {
console.log("Default: received send confirmation", event.data);
}
onRegisterResponse(event) {
console.log("Default: received register response", event.data);
}
onAuthenticateResponse(event) {
console.log("Default: received authentication response", event.data);
}
onErrorResponse(event) {
console.error("Received error response", event.data);
}
onUnknownResponse(event) {
console.error("Received unknown response", event);
}
// Gateway returns any received data from the mix network as a Blob,
// So most likely this is your best bet to override
onBlobResponse(event) {
// note that the actual handling depends on the expected content, however,
// in this example we're just sending a text messages to ourselves and hence
// we can safely read it as text
let reader = new FileReader();
reader.onload = () => {
this.onText(reader.result)
};
reader.readAsText(event.data);
}
/**
* @callback that makes a best-effort attempt to return decrypted Sphinx bytes as text.
*
* Note that no checks are performed to determine whether something is
* really text. If the received data is in fact binary, you'll get
* binary-as-text from this callback.
*/
onText(data) {
console.log("Default: parsed the following data", data);
}
}
/**
* Build a JSON registration request.
*
* @param {string} address
*/
function buildRegisterRequest(address) {
return JSON.stringify({ "type": "register", "address": address });
}
/**
* Build a JSON authentication request.
*
* @param {string} address
* @param {string} token
*/
function buildAuthenticateRequest(address, token) {
return JSON.stringify({ "type": "authenticate", "address": address, "token": token });
}
/**
* Make an HTTP request.
* @param {string} method
* @param {string} url
*/
function http(method, url) {
return new Promise(function (resolve, reject) {
let xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = function () {
if (this.status >= 200 && this.status < 300) {
resolve(xhr.response);
} else {
reject({
status: this.status,
statusText: xhr.statusText
});
}
};
xhr.onerror = function () {
reject({
status: this.status,
statusText: xhr.statusText
});
};
xhr.send();
});
}