331 lines
10 KiB
JavaScript
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();
|
|
});
|
|
}
|