username sanitisation for registration
This commit is contained in:
		
							parent
							
								
									48c2ef76f5
								
							
						
					
					
						commit
						7ee04d085f
					
				
					 8 changed files with 402 additions and 2350 deletions
				
			
		
							
								
								
									
										24
									
								
								.vscode/tasks.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.vscode/tasks.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,24 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						"version": "2.0.0",
 | 
				
			||||||
 | 
						"tasks": [
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								"type": "npm",
 | 
				
			||||||
 | 
								"script": "test",
 | 
				
			||||||
 | 
								"group": {
 | 
				
			||||||
 | 
									"kind": "build",
 | 
				
			||||||
 | 
									"isDefault": true
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								"problemMatcher": [],
 | 
				
			||||||
 | 
								"label": "npm: test",
 | 
				
			||||||
 | 
								"detail": "cross-env FORCE_COLOR=true supertape --format tap test/test.js | tap-dot",
 | 
				
			||||||
 | 
								"presentation": {
 | 
				
			||||||
 | 
									"echo": false,
 | 
				
			||||||
 | 
									"reveal": "always",
 | 
				
			||||||
 | 
									"focus": false,
 | 
				
			||||||
 | 
									"panel": "shared",
 | 
				
			||||||
 | 
									"showReuseMessage": false,
 | 
				
			||||||
 | 
									"clear": true
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -4,39 +4,16 @@ const assert = require("assert")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const passthrough = require("../../passthrough")
 | 
					const passthrough = require("../../passthrough")
 | 
				
			||||||
const { discord, sync, db } = passthrough
 | 
					const { discord, sync, db } = passthrough
 | 
				
			||||||
/** @type {import("../../matrix/mreq")} */
 | 
					/** @type {import("../../matrix/api")} */
 | 
				
			||||||
const mreq = sync.require("../../matrix/mreq")
 | 
					const api = sync.require("../../matrix/api")
 | 
				
			||||||
/** @type {import("../../matrix/file")} */
 | 
					/** @type {import("../../matrix/file")} */
 | 
				
			||||||
const file = sync.require("../../matrix/file")
 | 
					const file = sync.require("../../matrix/file")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function registerUser(username) {
 | 
					 | 
				
			||||||
	assert.ok(username.startsWith("_ooye_"))
 | 
					 | 
				
			||||||
	/** @type {import("../../types").R.Registered} */
 | 
					 | 
				
			||||||
	const res = await mreq.mreq("POST", "/client/v3/register", {
 | 
					 | 
				
			||||||
		type: "m.login.application_service",
 | 
					 | 
				
			||||||
		username
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	return res
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * A sim is an account that is being simulated by the bridge to copy events from the other side.
 | 
					 * A sim is an account that is being simulated by the bridge to copy events from the other side.
 | 
				
			||||||
 * @param {import("discord-api-types/v10").APIUser} user
 | 
					 * @param {import("discord-api-types/v10").APIUser} user
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
async function createSim(user) {
 | 
					async function createSim(user) {
 | 
				
			||||||
	assert.notEqual(user.discriminator, "0000", "user is not a webhook")
 | 
						assert.notEqual(user.discriminator, "0000", "user is not a webhook")
 | 
				
			||||||
	fetch("https://matrix.cadence.moe/_matrix/client/v3/register", {
 | 
						api.register("_ooye_example")
 | 
				
			||||||
		method: "POST",
 | 
					 | 
				
			||||||
		body: JSON.stringify({
 | 
					 | 
				
			||||||
			type: "m.login.application_service",
 | 
					 | 
				
			||||||
			username: "_ooye_example"
 | 
					 | 
				
			||||||
		}),
 | 
					 | 
				
			||||||
		headers: {
 | 
					 | 
				
			||||||
			Authorization: `Bearer ${reg.as_token}`
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
	}).then(res => res.text()).then(text => {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		console.log(text)
 | 
					 | 
				
			||||||
	}).catch(err => {
 | 
					 | 
				
			||||||
		console.log(err)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										74
									
								
								d2m/converters/user-to-mxid.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								d2m/converters/user-to-mxid.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,74 @@
 | 
				
			||||||
 | 
					// @ts-check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const assert = require("assert")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const passthrough = require("../../passthrough")
 | 
				
			||||||
 | 
					const { sync, db } = passthrough
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Downcased and stripped username. Can only include a basic set of characters.
 | 
				
			||||||
 | 
					 * https://spec.matrix.org/v1.6/appendices/#user-identifiers
 | 
				
			||||||
 | 
					 * @param {import("discord-api-types/v10").APIUser} user
 | 
				
			||||||
 | 
					 * @returns {string} localpart
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function downcaseUsername(user) {
 | 
				
			||||||
 | 
						// First, try to convert the username to the set of allowed characters
 | 
				
			||||||
 | 
						let downcased = user.username.toLowerCase()
 | 
				
			||||||
 | 
							// spaces to underscores...
 | 
				
			||||||
 | 
							.replace(/ /g, "_")
 | 
				
			||||||
 | 
							// remove disallowed characters...
 | 
				
			||||||
 | 
							.replace(/[^a-z0-9._=/-]*/g, "")
 | 
				
			||||||
 | 
							// remove leading and trailing dashes and underscores...
 | 
				
			||||||
 | 
							.replace(/(?:^[_-]*|[_-]*$)/g, "")
 | 
				
			||||||
 | 
						// The new length must be at least 2 characters (in other words, it should have some content)
 | 
				
			||||||
 | 
						if (downcased.length < 2) {
 | 
				
			||||||
 | 
							downcased = user.id
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return downcased
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** @param {string[]} preferences */
 | 
				
			||||||
 | 
					function* generateLocalpartAlternatives(preferences) {
 | 
				
			||||||
 | 
						const best = preferences[0]
 | 
				
			||||||
 | 
						assert.ok(best)
 | 
				
			||||||
 | 
						// First, suggest the preferences...
 | 
				
			||||||
 | 
						for (const localpart of preferences) {
 | 
				
			||||||
 | 
							yield localpart
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// ...then fall back to generating number suffixes...
 | 
				
			||||||
 | 
						let i = 2
 | 
				
			||||||
 | 
						while (true) {
 | 
				
			||||||
 | 
							yield best + (i++)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @param {import("discord-api-types/v10").APIUser} user
 | 
				
			||||||
 | 
					 * @returns {string}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function userToSimName(user) {
 | 
				
			||||||
 | 
						assert.notEqual(user.discriminator, "0000", "cannot create user for a webhook")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 1. Is sim user already registered?
 | 
				
			||||||
 | 
						const existing = db.prepare("SELECT sim_name FROM sim WHERE discord_id = ?").pluck().get(user.id)
 | 
				
			||||||
 | 
						if (existing) return existing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2. Register based on username (could be new or old format)
 | 
				
			||||||
 | 
						const downcased = downcaseUsername(user)
 | 
				
			||||||
 | 
						const preferences = [downcased]
 | 
				
			||||||
 | 
						if (user.discriminator.length === 4) { // Old style tag? If user.username is unavailable, try the full tag next
 | 
				
			||||||
 | 
							preferences.push(downcased + user.discriminator)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check for conflicts with already registered sims
 | 
				
			||||||
 | 
						/** @type {string[]} */
 | 
				
			||||||
 | 
						const matches = db.prepare("SELECT sim_name FROM sim WHERE sim_name LIKE ? ESCAPE '@'").pluck().all(downcased + "%")
 | 
				
			||||||
 | 
						// Keep generating until we get a suggestion that doesn't conflict
 | 
				
			||||||
 | 
						for (const suggestion of generateLocalpartAlternatives(preferences)) {
 | 
				
			||||||
 | 
							if (!matches.includes(suggestion)) return suggestion
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						throw new Error(`Ran out of suggestions when generating sim name. downcased: "${downcased}"`)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports.userToSimName = userToSimName
 | 
				
			||||||
							
								
								
									
										33
									
								
								d2m/converters/user-to-mxid.test.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								d2m/converters/user-to-mxid.test.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					const {test} = require("supertape")
 | 
				
			||||||
 | 
					const tryToCatch = require("try-to-catch")
 | 
				
			||||||
 | 
					const assert = require("assert")
 | 
				
			||||||
 | 
					const {userToSimName} = require("./user-to-mxid")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test("user2name: cannot create user for a webhook", async t => {
 | 
				
			||||||
 | 
					   const [error] = await tryToCatch(() => userToSimName({discriminator: "0000"}))
 | 
				
			||||||
 | 
					   t.ok(error instanceof assert.AssertionError, error.message)
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test("user2name: works on normal name", t => {
 | 
				
			||||||
 | 
					   t.equal(userToSimName({username: "Harry Styles!", discriminator: "0001"}), "harry_styles")
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test("user2name: works on emojis", t => {
 | 
				
			||||||
 | 
					   t.equal(userToSimName({username: "Cookie 🍪", discriminator: "0001"}), "cookie")
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test("user2name: works on crazy name", t => {
 | 
				
			||||||
 | 
					   t.equal(userToSimName({username: "*** D3 &W (89) _7//-", discriminator: "0001"}), "d3_w_89__7//")
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test("user2name: adds discriminator if name is unavailable (old tag format)", t => {
 | 
				
			||||||
 | 
					   t.equal(userToSimName({username: "BOT$", discriminator: "1234"}), "bot1234")
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test("user2name: adds number suffix if name is unavailable (new username format)", t => {
 | 
				
			||||||
 | 
					   t.equal(userToSimName({username: "bot", discriminator: "0"}), "bot2")
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test("user2name: uses ID if name becomes too short", t => {
 | 
				
			||||||
 | 
					   t.equal(userToSimName({username: "f***", discriminator: "0001", id: "9"}), "9")
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										20
									
								
								matrix/api.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								matrix/api.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					// @ts-check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const passthrough = require("../passthrough")
 | 
				
			||||||
 | 
					const { discord, sync, db } = passthrough
 | 
				
			||||||
 | 
					/** @type {import("./mreq")} */
 | 
				
			||||||
 | 
					const mreq = sync.require("./mreq")
 | 
				
			||||||
 | 
					/** @type {import("./file")} */
 | 
				
			||||||
 | 
					const file = sync.require("./file")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @returns {Promise<import("../types").R.Registered>}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function register(username) {
 | 
				
			||||||
 | 
					   return mreq.mreq("POST", "/client/v3/register", {
 | 
				
			||||||
 | 
					      type: "m.login.application_service",
 | 
				
			||||||
 | 
					      username
 | 
				
			||||||
 | 
					   })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports.register = register
 | 
				
			||||||
							
								
								
									
										2559
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										2559
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							| 
						 | 
					@ -18,21 +18,23 @@
 | 
				
			||||||
    "better-sqlite3": "^8.3.0",
 | 
					    "better-sqlite3": "^8.3.0",
 | 
				
			||||||
    "cloudstorm": "^0.7.0",
 | 
					    "cloudstorm": "^0.7.0",
 | 
				
			||||||
    "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#24508e701e91d5a00fa5e773ced874d9ee8c889b",
 | 
					    "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#24508e701e91d5a00fa5e773ced874d9ee8c889b",
 | 
				
			||||||
    "heatsync": "^2.4.0",
 | 
					    "heatsync": "^2.4.1",
 | 
				
			||||||
    "js-yaml": "^4.1.0",
 | 
					    "js-yaml": "^4.1.0",
 | 
				
			||||||
    "matrix-appservice": "^2.0.0",
 | 
					    "matrix-appservice": "^2.0.0",
 | 
				
			||||||
    "matrix-js-sdk": "^24.1.0",
 | 
					    "matrix-js-sdk": "^24.1.0",
 | 
				
			||||||
    "mixin-deep": "^2.0.1",
 | 
					    "mixin-deep": "^2.0.1",
 | 
				
			||||||
    "node-fetch": "^2.6.7",
 | 
					    "node-fetch": "^2.6.7",
 | 
				
			||||||
    "snowtransfer": "^0.7.0"
 | 
					    "snowtransfer": "^0.7.0",
 | 
				
			||||||
 | 
					    "try-to-catch": "^3.0.1"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@types/node": "^18.16.0",
 | 
					    "@types/node": "^18.16.0",
 | 
				
			||||||
    "@types/node-fetch": "^2.6.3",
 | 
					    "@types/node-fetch": "^2.6.3",
 | 
				
			||||||
 | 
					    "cross-env": "^7.0.3",
 | 
				
			||||||
    "supertape": "^8.3.0",
 | 
					    "supertape": "^8.3.0",
 | 
				
			||||||
    "tap-dot": "github:cloudrac3r/tap-dot#223a4e67a6f7daf015506a12a7af74605f06c7f4"
 | 
					    "tap-dot": "github:cloudrac3r/tap-dot#223a4e67a6f7daf015506a12a7af74605f06c7f4"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "test": "FORCE_COLOR=true supertape --format tap test/test.js | tap-dot"
 | 
					    "test": "cross-env FORCE_COLOR=true supertape --format tap test/test.js | tap-dot"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,8 +8,9 @@ const passthrough = require("../passthrough")
 | 
				
			||||||
const db = new sqlite("db/ooye.db")
 | 
					const db = new sqlite("db/ooye.db")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// @ts-ignore
 | 
					// @ts-ignore
 | 
				
			||||||
const sync = new HeatSync({persistent: false})
 | 
					const sync = new HeatSync({watchFS: false})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Object.assign(passthrough, { config, sync, db })
 | 
					Object.assign(passthrough, { config, sync, db })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require("../d2m/actions/create-room.test")
 | 
					require("../d2m/actions/create-room.test")
 | 
				
			||||||
 | 
					require("../d2m/converters/user-to-mxid.test")
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue