- Device ID is now stored inside the matrix-client-storage directory within Node-RED's config folder. This way if you leave the Device ID blank it will automatically generate one and save it. Previously if you didn't set a Device ID it would automatically generate one every time the client started which would break encryption (since device_id is tied to your encryption keys).

- You can now generate an Access Token and Device ID from just a username and password (use the Update Access Token button when configuring a client).
- Updated the look of the matrix-server-config node (icons and reorganization)
- Invalid auth token will no longer crash Node-RED on deployment/start. We do an authorized request before starting the client to see if the auth token is valid. If it is not the client will not start and all nodes for this client will show disconnected and an error will display in the debug toolbar.
- Updated matrix olm library from 3.2.1 to 3.2.8
- Setting a Device Label from the server-config node will now update your device's label. If you view the sessions for the client you will see it gets updated after deployment.
This commit is contained in:
Skylar Sadlier 2022-01-28 10:53:10 -07:00
parent 25c92b787a
commit 58d7d23b2f
5 changed files with 620 additions and 2379 deletions

1
.npmrc Normal file
View File

@ -0,0 +1 @@
@matrix-org:registry=https://gitlab.matrix.org/api/v4/packages/npm/

2726
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,12 +3,12 @@
"version": "0.3.0", "version": "0.3.0",
"description": "Matrix chat server client for Node-RED", "description": "Matrix chat server client for Node-RED",
"dependencies": { "dependencies": {
"@matrix-org/olm": "^3.2.8",
"fs-extra": "^9.1.0", "fs-extra": "^9.1.0",
"got": "^11.8.2", "got": "^11.8.2",
"isomorphic-webcrypto": "^2.3.8", "isomorphic-webcrypto": "^2.3.8",
"matrix-js-sdk": "^15.3.0", "matrix-js-sdk": "^15.3.0",
"node-localstorage": "^2.2.1", "node-localstorage": "^2.2.1",
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"process": "^0.11.10", "process": "^0.11.10",
"utf8": "^3.0.0" "utf8": "^3.0.0"
}, },

View File

@ -1,9 +1,33 @@
<style>
.matrix-loader {
border: 8px solid #f3f3f3;
border-radius: 50%;
border-top: 8px solid #3498db;
border-bottom: 8px solid #3498db;
width: 20px;
height: 20px;
-webkit-animation: matrix-spin 2s linear infinite;
animation: matrix-spin 2s linear infinite;
}
@-webkit-keyframes matrix-spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes matrix-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<script type="text/javascript"> <script type="text/javascript">
RED.nodes.registerType('matrix-server-config',{ RED.nodes.registerType('matrix-server-config',{
category: 'config', category: 'config',
color: '#00b7ca', color: '#00b7ca',
credentials: { credentials: {
userId: { type: "text", required: true }, userId: { type: "text", required: true },
deviceLabel: { type: "text", required: false },
accessToken: { type: "password", required: true }, accessToken: { type: "password", required: true },
deviceId: { type: "text", required: true }, deviceId: { type: "text", required: true },
url: { type: "text", required: true } url: { type: "text", required: true }
@ -26,28 +50,51 @@
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label> <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-config-input-name" placeholder="Name"> <input type="text" id="node-config-input-name" placeholder="Name">
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-config-input-userId"><i class="fa fa-server"></i> User ID</label> <label for="node-config-input-deviceLabel"><i class="fa fa-tag"></i> Device Label</label>
<input type="text" placeholder="Node-RED" id="node-config-input-deviceLabel">
</div>
<div class="form-row">
<label for="node-config-input-url"><i class="fa fa-server"></i> Server URL</label>
<input type="text" placeholder="https://matrix.org" id="node-config-input-url">
</div>
<div class="form-row">
<label for="node-config-input-userId"><i class="fa fa-user"></i> User ID</label>
<input type="text" placeholder="@example:matrix.org" id="node-config-input-userId"> <input type="text" placeholder="@example:matrix.org" id="node-config-input-userId">
</div> </div>
<div class="form-row">
<label for="node-config-input-password"><i class="fa fa-key"></i> Password</label>
<input type="password" placeholder="" id="node-config-input-password">
</div>
<div class="form-tips" style="margin-bottom: 12px;">
Password is never saved and is only used to fetch an access token using the button below.
</div>
<pre class="form-tips" id="matrix-chat-login-error" style="color: #721c24;background-color: #f8d7da;border-color: #f5c6cb;margin-bottom: 12px;display:none;"></pre>
<pre class="form-tips" id="matrix-chat-login-success" style="color: #155724;background-color: #d4edda;border-color: #c3e6cb;margin-bottom: 12px;display:none;"></pre>
<button class="ui-button ui-corner-all ui-widget primary" id="matrix-login-btn">Update Access Token</button>
<div class="matrix-loader" id="matrix-access-token-loader" style="display:none;"></div>
<div class="form-row"> <div class="form-row">
<label for="node-config-input-accessToken"><i class="fa fa-key"></i> Access Token</label> <label for="node-config-input-accessToken"><i class="fa fa-key"></i> Access Token</label>
<input type="text" id="node-config-input-accessToken"> <input type="text" id="node-config-input-accessToken">
</div> </div>
<div class="form-tips" style="margin-bottom: 12px;"> <div class="form-tips" style="margin-bottom: 12px;">
View the <a href="javascript:$('#red-ui-tab-help-link-button').click();">node docs</a> to figure out how to generate an Access Token. You can also generate them using the Shared Secret Registration node. You can either provide/generate an access token yourself or use the login button above to do it automatically. View the <a href="javascript:$('#red-ui-tab-help-link-button').click();">node docs</a> to figure out how to generate an Access Token manually. If you generated a user with shared secret registration you will already have an access token you can place here.
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-config-input-deviceId"><i class="fa fa-key"></i> Device ID</label> <label for="node-config-input-deviceId"><i class="fa fa-desktop"></i> Device ID</label>
<input type="text" id="node-config-input-deviceId"> <input type="text" id="node-config-input-deviceId">
</div> </div>
<div class="form-tips" style="margin-bottom: 12px;"> <div class="form-tips" style="margin-bottom: 12px;">
This can either be an existing Device ID attached to the above Access Token or you can enter a unique value to set a new one. If you have a Device ID for this auth token you can place it here, or you can leave it blank to have one automatically generated (this field will always be blank if auto generated as it is saved internally).<br />
WARNING: If you change this after the client has already initialized you will break encryption.
</div> </div>
<div class="form-row">
<label for="node-config-input-url"><i class="fa fa-globe"></i> Server URL</label>
<input type="text" placeholder="https://matrix.org" id="node-config-input-url">
</div>
<div class="form-row"> <div class="form-row">
<input <input
type="checkbox" type="checkbox"
@ -79,10 +126,79 @@
Global access to Matrix Client Global access to Matrix Client
</label> </label>
<div class="form-tips" style="margin-bottom: 12px;"> <div class="form-tips" style="margin-bottom: 12px;">
If enabled this allows you to access the matrix client directly with a Function node. This way you can do <a href="https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#use-function-node-to-run-any-command" target="_blank">whatever you want</a> with the client. Example:<br> If global access is enabled you can access the client directly within a Function node. This way you can do <a href="https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#use-function-node-to-run-any-command" target="_blank">whatever you want</a> with the client. Example:<br>
<code style="white-space: normal;">let client = global.get("matrixClient['@bot:example.com']");</code> <code style="white-space: normal;">let client = global.get("matrixClient['@bot:example.com']");</code>
</div> </div>
</div> </div>
<script type="text/javascript">
$("#matrix-login-btn").on("click", function() {
function prettyPrintJson(json) {
try{
return typeof json === 'object' ? JSON.stringify(json, null, 2) : json;
}
catch (error){
return json;
}
}
let userId = $("#node-config-input-userId").val(),
userPassword = $("#node-config-input-password").val(),
serverUrl = $("#node-config-input-url").val();
if(!userId) {
alert("User ID is required to fetch access token.");
return;
}
if(!userPassword) {
alert("Password is required to fetch access token.");
return;
}
if(!serverUrl) {
alert("Server URL is required to fetch access token.");
return;
}
$("#matrix-login-btn, #matrix-chat-login-error, #matrix-chat-login-success").hide();
$("#matrix-access-token-loader").show();
$.ajax({
type: 'POST',
url: '/matrix-chat/login',
dataType: 'json',
data: {
'userId': userId,
'password': userPassword,
'baseUrl': serverUrl,
'displayName': $("#node-config-input-deviceLabel").val(),
}
}).then(
function(data) {
if(data.result && data.result === 'ok') {
$("#matrix-chat-login-error").hide();
$("#matrix-chat-login-success")
.html("Login Successful! Auth Token and Device ID have been set below.")
.show();
$("#node-config-input-accessToken").val(data.token);
$("#node-config-input-deviceId").val(data.device_id);
} else if(data.result && data.result === 'error') {
$("#matrix-chat-login-success").hide();
$("#matrix-chat-login-error")
.html(data.message ? ('Failed to login: <br />' + prettyPrintJson(data.message)) : 'Failed to login')
.show();
}
$("#matrix-login-btn").show();
$("#matrix-access-token-loader").hide();
}, function() {
$("#matrix-chat-login-success").hide();
$("#matrix-chat-login-error")
.html("Failed to login due to server error communicating with Node-RED")
.show();
$("#matrix-login-btn").show();
$("#matrix-access-token-loader").hide();
}
);
});
</script>
</script> </script>
<script type="text/html" data-help-name="matrix-server-config"> <script type="text/html" data-help-name="matrix-server-config">

View File

@ -1,4 +1,4 @@
global.Olm = require('olm'); global.Olm = require('@matrix-org/olm');
const fs = require("fs-extra"); const fs = require("fs-extra");
const sdk = require("matrix-js-sdk"); const sdk = require("matrix-js-sdk");
const { LocalStorage } = require('node-localstorage'); const { LocalStorage } = require('node-localstorage');
@ -29,6 +29,7 @@ module.exports = function(RED) {
this.connected = null; this.connected = null;
this.name = n.name; this.name = n.name;
this.userId = this.credentials.userId; this.userId = this.credentials.userId;
this.deviceLabel = this.credentials.deviceLabel || null;
this.deviceId = this.credentials.deviceId || null; this.deviceId = this.credentials.deviceId || null;
this.url = this.credentials.url; this.url = this.credentials.url;
this.autoAcceptRoomInvites = n.autoAcceptRoomInvites; this.autoAcceptRoomInvites = n.autoAcceptRoomInvites;
@ -36,6 +37,9 @@ module.exports = function(RED) {
this.e2ee = (this.enableE2ee && this.deviceId); this.e2ee = (this.enableE2ee && this.deviceId);
this.globalAccess = n.global; this.globalAccess = n.global;
this.initializedAt = new Date(); this.initializedAt = new Date();
let localStorageDir = storageDir + '/' + MatrixFolderNameFromUserId(this.userId),
localStorage = new LocalStorage(localStorageDir),
initialSetup = false;
if(!this.credentials.accessToken) { if(!this.credentials.accessToken) {
node.log("Matrix connection failed: missing access token."); node.log("Matrix connection failed: missing access token.");
@ -54,6 +58,40 @@ module.exports = function(RED) {
if (connected) { if (connected) {
node.log("Matrix server connection ready."); node.log("Matrix server connection ready.");
node.emit("connected"); node.emit("connected");
if(!initialSetup) {
// store Device ID internally
let stored_device_id = getStoredDeviceId(localStorage),
device_id = this.matrixClient.getDeviceId();
if(!stored_device_id || stored_device_id !== device_id) {
node.log(`Saving Device ID (old:${stored_device_id} new:${device_id})`);
storeDeviceId(localStorage, device_id);
}
// update device label
if(node.deviceLabel) {
node.matrixClient
.getDevice(device_id)
.then(
function(response) {
if(response.display_name !== node.deviceLabel) {
node.matrixClient.setDeviceDetails(device_id, {
display_name: node.deviceLabel
}).then(
function(response) {},
function(error) {
node.error("Failed to set device label: " + error);
}
);
}
},
function(error) {
node.error("Failed to fetch device: " + error);
}
);
}
initialSetup = true;
}
} else { } else {
node.emit("disconnected"); node.emit("disconnected");
} }
@ -65,18 +103,15 @@ module.exports = function(RED) {
}; };
node.setConnected(false); node.setConnected(false);
let localStorageDir = storageDir + '/' + MatrixFolderNameFromUserId(this.userId);
fs.ensureDirSync(storageDir); // create storage directory if it doesn't exist fs.ensureDirSync(storageDir); // create storage directory if it doesn't exist
upgradeDirectoryIfNecessary(node, storageDir); upgradeDirectoryIfNecessary(node, storageDir);
const localStorage = new LocalStorage(localStorageDir);
node.matrixClient = sdk.createClient({ node.matrixClient = sdk.createClient({
baseUrl: this.url, baseUrl: this.url,
accessToken: this.credentials.accessToken, accessToken: this.credentials.accessToken,
sessionStore: new sdk.WebStorageSessionStore(localStorage), sessionStore: new sdk.WebStorageSessionStore(localStorage),
cryptoStore: new LocalStorageCryptoStore(localStorage), cryptoStore: new LocalStorageCryptoStore(localStorage),
userId: this.userId, userId: this.userId,
deviceId: this.deviceId || undefined, deviceId: (this.deviceId || getStoredDeviceId(localStorage)) || undefined
}); });
// set globally if configured to do so // set globally if configured to do so
@ -84,12 +119,15 @@ module.exports = function(RED) {
this.context().global.set('matrixClient["'+this.userId+'"]', node.matrixClient); this.context().global.set('matrixClient["'+this.userId+'"]', node.matrixClient);
} }
node.on('close', function(done) { function stopClient() {
if(node.matrixClient) { if(node.matrixClient && node.matrixClient.clientRunning) {
node.matrixClient.stopClient(); node.matrixClient.stopClient();
node.setConnected(false); node.setConnected(false);
} }
}
node.on('close', function(done) {
stopClient();
done(); done();
}); });
@ -130,10 +168,20 @@ module.exports = function(RED) {
event : event, event : event,
}; };
node.log("Received" + (msg.encrypted ? ' encrypted' : '') +" timeline event [" + msg.type + "]: (" + room.name + ") " + event.getSender() + " :: " + msg.content.body + (data.liveEvent ? ' [LIVE]' : '') + (toStartOfTimeline ? ' [PAGINATED]' : '')); node.log("Received" + (msg.encrypted ? ' encrypted' : '') +" timeline event [" + msg.type + "]: (" + room.name + ") " + event.getSender() + " :: " + msg.content.body + (toStartOfTimeline ? ' [PAGINATED]' : ''));
node.emit("Room.timeline", event, room, toStartOfTimeline, removed, data, msg); node.emit("Room.timeline", event, room, toStartOfTimeline, removed, data, msg);
}); });
/**
* Fires when we want to suggest to the user that they restore their megolm keys
* from backup or by cross-signing the device.
*
* @event module:client~MatrixClient#"crypto.suggestKeyRestore"
*/
node.matrixClient.on("crypto.suggestKeyRestore", function(){
});
// node.matrixClient.on("RoomMember.typing", async function(event, member) { // node.matrixClient.on("RoomMember.typing", async function(event, member) {
// let isTyping = member.typing; // let isTyping = member.typing;
// let roomId = member.roomId; // let roomId = member.roomId;
@ -245,7 +293,8 @@ module.exports = function(RED) {
// httpStatus: 401 // httpStatus: 401
// } // }
node.error("[Session.logged_out] " + errorObj); node.error("Authentication failure: " + errorObj);
stopClient();
}); });
async function run() { async function run() {
@ -259,24 +308,72 @@ module.exports = function(RED) {
await node.matrixClient.startClient({ await node.matrixClient.startClient({
initialSyncLimit: 8 initialSyncLimit: 8
}); });
} catch(error){ } catch(error) {
node.error(error); node.error(error);
} }
} }
run().catch((error) => node.error(error)); // do an authed request and only continue if we don't get an error
// this prevent the matrix client from crashing Node-RED on invalid auth token
node.matrixClient.getAccountDataFromServer()
.then(
function() {
run().catch((error) => node.error(error));
},
function(err) {}
);
} }
} }
RED.nodes.registerType("matrix-server-config", MatrixServerNode, { RED.nodes.registerType("matrix-server-config", MatrixServerNode, {
credentials: { credentials: {
userId: { type:"text", required: true }, deviceLabel: { type: "text", required: false },
accessToken: { type:"text", required: true }, userId: { type: "text", required: true },
accessToken: { type: "text", required: true },
deviceId: { type: "text", required: true }, deviceId: { type: "text", required: true },
url: { type: "text", required: true }, url: { type: "text", required: true }
} }
}); });
RED.httpAdmin.post(
"/matrix-chat/login",
RED.auth.needsPermission('flows.write'),
function(req, res) {
let userId = req.body.userId || undefined,
password = req.body.password || undefined,
baseUrl = req.body.baseUrl || undefined,
deviceId = req.body.deviceId || undefined,
displayName = req.body.displayName || undefined;
const matrixClient = sdk.createClient({
baseUrl: baseUrl,
deviceId: deviceId
});
matrixClient.login(
'm.login.password', {
user: userId,
password: password,
initial_device_display_name: displayName
})
.then(
function(response) {
res.json({
'result': 'ok',
'token': response.access_token,
'device_id': response.device_id,
'user_id': response.user_id,
});
},
function(err) {
res.json({
'result': 'error',
'message': err
});
}
);
});
function upgradeDirectoryIfNecessary(node, storageDir) { function upgradeDirectoryIfNecessary(node, storageDir) {
let oldStorageDir = './matrix-local-storage'; let oldStorageDir = './matrix-local-storage';
@ -302,4 +399,15 @@ module.exports = function(RED) {
fs.renameSync(oldStorageDir, oldStorageDir + "-backup"); fs.renameSync(oldStorageDir, oldStorageDir + "-backup");
} }
} }
/**
* If a device ID is stored we will use that for the client
*/
function getStoredDeviceId(localStorage) {
return localStorage.getItem('my_device_id');
}
function storeDeviceId(localStorage, deviceId) {
localStorage.setItem('my_device_id', deviceId);
}
} }