matrix-nodejs-homeserver 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +75 -0
- package/README.md +457 -2
- package/dist/adapters/auth.d.ts +50 -0
- package/dist/adapters/auth.js +18 -0
- package/dist/adapters/cluster.d.ts +55 -0
- package/dist/adapters/cluster.js +249 -0
- package/dist/adapters/media.d.ts +50 -0
- package/dist/adapters/media.js +63 -0
- package/dist/adapters/s3.d.ts +33 -0
- package/dist/adapters/s3.js +95 -0
- package/dist/api/account.js +560 -0
- package/dist/api/admin.js +251 -0
- package/dist/api/appservice.js +33 -0
- package/dist/api/backup.js +269 -0
- package/dist/api/federation.js +792 -0
- package/dist/api/filters.js +87 -0
- package/dist/api/index.js +47 -0
- package/dist/api/legacy.js +170 -0
- package/dist/api/media.js +255 -0
- package/dist/api/misc.js +86 -0
- package/dist/api/preview.js +200 -0
- package/dist/api/profile.js +165 -0
- package/dist/api/push.js +187 -0
- package/dist/api/relationships.js +264 -0
- package/dist/api/rooms.js +1364 -0
- package/dist/api/rtc.d.ts +24 -0
- package/dist/api/rtc.js +52 -0
- package/dist/api/search.js +155 -0
- package/dist/api/slidingsync.js +464 -0
- package/dist/api/spaces.js +172 -0
- package/dist/api/sync.js +600 -0
- package/dist/api/threadsubs.js +129 -0
- package/dist/api/threepid.js +122 -0
- package/dist/api/uia.js +126 -0
- package/dist/api/user.js +286 -0
- package/dist/core/admin.d.ts +85 -0
- package/dist/core/admin.js +307 -0
- package/dist/core/adminroom.d.ts +49 -0
- package/dist/core/adminroom.js +482 -0
- package/dist/core/appservices.d.ts +73 -0
- package/dist/core/appservices.js +368 -0
- package/dist/core/audit.d.ts +51 -0
- package/dist/core/audit.js +86 -0
- package/dist/core/auth.d.ts +48 -0
- package/dist/core/auth.js +237 -0
- package/dist/core/authrules.d.ts +4 -0
- package/dist/core/authrules.js +347 -0
- package/dist/core/cluster.d.ts +66 -0
- package/dist/core/cluster.js +191 -0
- package/dist/core/crypto.d.ts +10 -0
- package/dist/core/crypto.js +100 -0
- package/dist/core/delayed.d.ts +39 -0
- package/dist/core/delayed.js +159 -0
- package/dist/core/errors.d.ts +9 -0
- package/dist/core/errors.js +45 -0
- package/dist/core/events.d.ts +32 -0
- package/dist/core/events.js +185 -0
- package/dist/core/glob.js +66 -0
- package/dist/core/identity.d.ts +24 -0
- package/dist/core/identity.js +96 -0
- package/dist/core/ids.js +36 -0
- package/dist/core/invitefilter.js +35 -0
- package/dist/core/json.js +16 -0
- package/dist/core/keys.d.ts +36 -0
- package/dist/core/keys.js +449 -0
- package/dist/core/notifier.d.ts +17 -0
- package/dist/core/notifier.js +54 -0
- package/dist/core/oauth.d.ts +38 -0
- package/dist/core/oauth.js +108 -0
- package/dist/core/partialstate.d.ts +32 -0
- package/dist/core/partialstate.js +197 -0
- package/dist/core/phone.js +268 -0
- package/dist/core/policy.d.ts +29 -0
- package/dist/core/policy.js +96 -0
- package/dist/core/profile.js +39 -0
- package/dist/core/push.d.ts +47 -0
- package/dist/core/push.js +306 -0
- package/dist/core/pushrules.d.ts +1 -0
- package/dist/core/pushrules.js +228 -0
- package/dist/core/ratelimit.d.ts +23 -0
- package/dist/core/ratelimit.js +77 -0
- package/dist/core/rooms.d.ts +215 -0
- package/dist/core/rooms.js +1729 -0
- package/dist/core/settings.d.ts +11 -0
- package/dist/core/settings.js +29 -0
- package/dist/core/sso.d.ts +46 -0
- package/dist/core/sso.js +165 -0
- package/dist/core/stateres.d.ts +4 -0
- package/dist/core/stateres.js +208 -0
- package/dist/core/threepid.d.ts +80 -0
- package/dist/core/threepid.js +275 -0
- package/dist/core/typing.d.ts +16 -0
- package/dist/core/typing.js +60 -0
- package/dist/federation/client.d.ts +51 -0
- package/dist/federation/client.js +278 -0
- package/dist/federation/index.d.ts +153 -0
- package/dist/federation/index.js +1058 -0
- package/dist/federation/keyring.d.ts +26 -0
- package/dist/federation/keyring.js +122 -0
- package/dist/federation/sender.d.ts +26 -0
- package/dist/federation/sender.js +164 -0
- package/dist/homeserver.d.ts +223 -0
- package/dist/homeserver.js +301 -0
- package/dist/http/adapters.d.ts +40 -0
- package/dist/http/adapters.js +130 -0
- package/dist/http/handler.d.ts +8 -0
- package/dist/http/handler.js +202 -0
- package/dist/http/input.js +24 -0
- package/dist/http/router.d.ts +60 -0
- package/dist/http/router.js +76 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +14 -0
- package/dist/storage/memory.d.ts +5 -0
- package/dist/storage/memory.js +122 -0
- package/dist/storage/schema.d.ts +4 -0
- package/dist/storage/schema.js +664 -0
- package/dist/storage/sql.d.ts +72 -0
- package/dist/storage/sql.js +274 -0
- package/dist/storage/types.d.ts +55 -0
- package/package.json +67 -4
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import { isObject } from "../core/json.js";
|
|
2
|
+
import { randomString } from "../core/crypto.js";
|
|
3
|
+
import { MatrixError, errors } from "../core/errors.js";
|
|
4
|
+
import { LOCALPART_RE } from "../core/ids.js";
|
|
5
|
+
import { toMsisdn } from "../core/phone.js";
|
|
6
|
+
import { canonicalAddress, isMedium } from "../core/threepid.js";
|
|
7
|
+
import { requireUia } from "./uia.js";
|
|
8
|
+
import { body, fail, lenient } from "../http/input.js";
|
|
9
|
+
import { createHmac, randomInt, timingSafeEqual } from "node:crypto";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
//#region src/api/account.ts
|
|
12
|
+
const deviceFields = {
|
|
13
|
+
device_id: lenient(z.string()),
|
|
14
|
+
initial_device_display_name: lenient(z.string()),
|
|
15
|
+
refresh_token: lenient(z.boolean()),
|
|
16
|
+
inhibit_login: lenient(z.boolean())
|
|
17
|
+
};
|
|
18
|
+
const loginBody = body({
|
|
19
|
+
type: lenient(z.string()),
|
|
20
|
+
token: lenient(z.string()),
|
|
21
|
+
password: lenient(z.string()),
|
|
22
|
+
...deviceFields
|
|
23
|
+
});
|
|
24
|
+
const registerBody = body({
|
|
25
|
+
type: lenient(z.string()),
|
|
26
|
+
auth: lenient(z.looseObject({ type: lenient(z.string()) })),
|
|
27
|
+
username: z.string(fail("M_INVALID_PARAM", "username must be a string")).optional(),
|
|
28
|
+
password: z.string(fail("M_INVALID_PARAM", "password must be a string")).optional(),
|
|
29
|
+
...deviceFields
|
|
30
|
+
});
|
|
31
|
+
const passwordBody = body({
|
|
32
|
+
new_password: lenient(z.string()),
|
|
33
|
+
logout_devices: lenient(z.boolean())
|
|
34
|
+
});
|
|
35
|
+
const sharedSecretRegisterBody = body({
|
|
36
|
+
nonce: lenient(z.string()),
|
|
37
|
+
username: z.string(fail("M_INVALID_PARAM", "Invalid username/password")),
|
|
38
|
+
password: z.string(fail("M_INVALID_PARAM", "Invalid username/password")),
|
|
39
|
+
admin: lenient(z.boolean()),
|
|
40
|
+
mac: lenient(z.string()),
|
|
41
|
+
displayname: lenient(z.string())
|
|
42
|
+
});
|
|
43
|
+
function loginFlows(hs) {
|
|
44
|
+
if (hs.oauth) return [];
|
|
45
|
+
const flows = [];
|
|
46
|
+
if (hs.userAuth.passwordLoginEnabled) flows.push({ type: "m.login.password" });
|
|
47
|
+
if (hs.sso) flows.push({
|
|
48
|
+
type: "m.login.sso",
|
|
49
|
+
identity_providers: hs.sso.providers.map((p) => ({
|
|
50
|
+
id: p.id,
|
|
51
|
+
name: p.name,
|
|
52
|
+
icon: p.icon,
|
|
53
|
+
brand: p.brand
|
|
54
|
+
}))
|
|
55
|
+
});
|
|
56
|
+
flows.push({
|
|
57
|
+
type: "m.login.token",
|
|
58
|
+
...hs.config.loginViaExistingSession ? { get_login_token: true } : {}
|
|
59
|
+
});
|
|
60
|
+
return flows;
|
|
61
|
+
}
|
|
62
|
+
/** Endpoints that manage passwords are unavailable when an OAuth provider owns the accounts. */
|
|
63
|
+
function rejectIfDelegated(hs) {
|
|
64
|
+
if (hs.oauth) throw new MatrixError(403, "M_FORBIDDEN", "Accounts are managed by the OAuth provider", hs.oauth.options.accountManagementUrl ? { account_management_uri: hs.oauth.options.accountManagementUrl } : {});
|
|
65
|
+
}
|
|
66
|
+
async function validateNewUsername(hs, raw) {
|
|
67
|
+
if (typeof raw !== "string" || raw.length === 0) throw errors.invalidUsername("Username must be a non-empty string");
|
|
68
|
+
const localpart = raw.startsWith("@") ? raw.slice(1).split(":")[0] : raw;
|
|
69
|
+
if (!LOCALPART_RE.test(localpart)) throw errors.invalidUsername("Username contains invalid characters");
|
|
70
|
+
if (`@${localpart}:${hs.serverName}`.length > 255) throw errors.invalidUsername("Username too long");
|
|
71
|
+
if (!await hs.userAuth.validateUsername(localpart)) throw errors.invalidUsername();
|
|
72
|
+
if (await hs.db.findOne("users", [{
|
|
73
|
+
field: "user_id",
|
|
74
|
+
value: `@${localpart}:${hs.serverName}`
|
|
75
|
+
}])) throw errors.userInUse();
|
|
76
|
+
return localpart;
|
|
77
|
+
}
|
|
78
|
+
async function generateLocalpart(hs) {
|
|
79
|
+
for (;;) {
|
|
80
|
+
const candidate = String(randomInt(1e9));
|
|
81
|
+
if (!await hs.db.findOne("users", [{
|
|
82
|
+
field: "localpart",
|
|
83
|
+
value: candidate
|
|
84
|
+
}])) return candidate;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function createUser(hs, localpart, opts) {
|
|
88
|
+
const userId = `@${localpart}:${hs.serverName}`;
|
|
89
|
+
await hs.db.create("users", {
|
|
90
|
+
user_id: userId,
|
|
91
|
+
localpart,
|
|
92
|
+
password_hash: opts.password ? await hs.userAuth.hashPassword(opts.password) : null,
|
|
93
|
+
displayname: opts.displayname ?? localpart,
|
|
94
|
+
avatar_url: null,
|
|
95
|
+
is_guest: !!opts.guest,
|
|
96
|
+
is_admin: !!opts.admin,
|
|
97
|
+
deactivated: false,
|
|
98
|
+
created_ts: Date.now()
|
|
99
|
+
});
|
|
100
|
+
await hs.userAuth.onUserRegistered(userId);
|
|
101
|
+
await hs.adminRoom?.onUserCreated(userId).catch((err) => hs.logger.error("Admin room setup failed", err));
|
|
102
|
+
return userId;
|
|
103
|
+
}
|
|
104
|
+
async function resolveLoginUser(hs, body) {
|
|
105
|
+
let user;
|
|
106
|
+
const id = isObject(body.identifier) ? body.identifier : null;
|
|
107
|
+
let threepid = null;
|
|
108
|
+
if (id?.type === "m.id.thirdparty" && typeof id.medium === "string" && typeof id.address === "string") threepid = {
|
|
109
|
+
medium: id.medium,
|
|
110
|
+
address: isMedium(id.medium) ? canonicalAddress(id.medium, id.address) : null
|
|
111
|
+
};
|
|
112
|
+
else if (id?.type === "m.id.phone" && typeof id.country === "string" && typeof id.phone === "string") threepid = {
|
|
113
|
+
medium: "msisdn",
|
|
114
|
+
address: toMsisdn(id.country, id.phone)
|
|
115
|
+
};
|
|
116
|
+
else if (!id && typeof body.medium === "string" && typeof body.address === "string") threepid = {
|
|
117
|
+
medium: body.medium,
|
|
118
|
+
address: isMedium(body.medium) ? canonicalAddress(body.medium, body.address) : null
|
|
119
|
+
};
|
|
120
|
+
if (threepid) {
|
|
121
|
+
const owner = threepid.address && isMedium(threepid.medium) ? await hs.threepids.userFor(threepid.medium, threepid.address) : null;
|
|
122
|
+
if (!owner) throw errors.forbidden("Invalid username or password");
|
|
123
|
+
return owner;
|
|
124
|
+
}
|
|
125
|
+
if (id) {
|
|
126
|
+
if (id.type !== "m.id.user") throw errors.unknown("Unsupported identifier type");
|
|
127
|
+
user = id.user;
|
|
128
|
+
} else user = body.user;
|
|
129
|
+
if (typeof user !== "string" || !user) throw errors.missingParam("Missing user identifier");
|
|
130
|
+
if (user.startsWith("@")) {
|
|
131
|
+
const [lp, ...server] = user.slice(1).split(":");
|
|
132
|
+
if (server.join(":") !== hs.serverName) throw errors.forbidden("Invalid username or password");
|
|
133
|
+
return `@${lp.toLowerCase()}:${hs.serverName}`;
|
|
134
|
+
}
|
|
135
|
+
return `@${user.toLowerCase()}:${hs.serverName}`;
|
|
136
|
+
}
|
|
137
|
+
function registerAccountRoutes(r) {
|
|
138
|
+
r.client("GET", "/login", (ctx) => ({ flows: loginFlows(ctx.hs) }));
|
|
139
|
+
r.client("GET", "/auth_metadata", async (ctx) => {
|
|
140
|
+
if (!ctx.hs.oauth) throw errors.unrecognized("OAuth is not enabled");
|
|
141
|
+
return ctx.hs.oauth.serverMetadata();
|
|
142
|
+
}, ["v1"]);
|
|
143
|
+
r.client("GET", "/auth_issuer", (ctx) => {
|
|
144
|
+
if (!ctx.hs.oauth) throw errors.unrecognized("OAuth is not enabled");
|
|
145
|
+
return { issuer: ctx.hs.oauth.options.issuer };
|
|
146
|
+
}, ["v1"]);
|
|
147
|
+
const ssoRedirect = async (ctx) => {
|
|
148
|
+
if (!ctx.hs.sso) throw errors.unrecognized("SSO is not enabled");
|
|
149
|
+
const redirectUrl = ctx.query.get("redirectUrl");
|
|
150
|
+
if (!redirectUrl || !/^https?:\/\/|^[a-z][a-z0-9+.-]*:/i.test(redirectUrl)) throw errors.missingParam("Missing or invalid redirectUrl");
|
|
151
|
+
return Response.redirect(await ctx.hs.sso.startLogin(ctx.params.idpId ?? null, redirectUrl), 302);
|
|
152
|
+
};
|
|
153
|
+
r.client("POST", "/login/get_token", async (ctx) => {
|
|
154
|
+
const s = await ctx.auth();
|
|
155
|
+
if (!ctx.hs.config.loginViaExistingSession || ctx.hs.oauth || s.isGuest) throw errors.unrecognized();
|
|
156
|
+
await ctx.rateLimit("login");
|
|
157
|
+
await requireUia(ctx.hs, await ctx.json(), [["m.login.password"]], s.userId);
|
|
158
|
+
const ttl = 12e4;
|
|
159
|
+
return {
|
|
160
|
+
login_token: await ctx.hs.auth.createLoginToken(s.userId, ttl),
|
|
161
|
+
expires_in_ms: ttl
|
|
162
|
+
};
|
|
163
|
+
}, ["v1"]);
|
|
164
|
+
r.client("GET", "/login/sso/redirect", ssoRedirect);
|
|
165
|
+
r.client("GET", "/login/sso/redirect/:idpId", ssoRedirect);
|
|
166
|
+
r.client("GET", "/login/sso/callback/:idpId", async (ctx) => {
|
|
167
|
+
if (!ctx.hs.sso) throw errors.unrecognized("SSO is not enabled");
|
|
168
|
+
return Response.redirect(await ctx.hs.sso.finishLogin(ctx.params.idpId, ctx.query), 302);
|
|
169
|
+
});
|
|
170
|
+
r.client("POST", "/login", { input: loginBody }, async (ctx) => {
|
|
171
|
+
await ctx.rateLimit("login");
|
|
172
|
+
const body = ctx.input;
|
|
173
|
+
const hs = ctx.hs;
|
|
174
|
+
if (hs.oauth) throw errors.unrecognized("Log in through the OAuth provider");
|
|
175
|
+
if (body.type === "m.login.application_service") {
|
|
176
|
+
const reg = ctx.accessToken() ? hs.appservices.byToken(ctx.accessToken()) : null;
|
|
177
|
+
if (!reg) throw errors.unknownToken("An application service token is required");
|
|
178
|
+
const userId = await resolveLoginUser(hs, body);
|
|
179
|
+
if (!hs.appservices.isInterestedUser(reg, userId)) throw errors.forbidden("User is not in the application service's namespace");
|
|
180
|
+
if (!await hs.db.findOne("users", [{
|
|
181
|
+
field: "user_id",
|
|
182
|
+
value: userId
|
|
183
|
+
}])) throw errors.forbidden("Unknown user");
|
|
184
|
+
return {
|
|
185
|
+
...await hs.auth.createSession(userId, {
|
|
186
|
+
deviceId: body.device_id,
|
|
187
|
+
displayName: body.initial_device_display_name
|
|
188
|
+
}),
|
|
189
|
+
home_server: hs.serverName
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (body.type === "m.login.token") {
|
|
193
|
+
if (body.token === void 0) throw errors.missingParam("Missing token");
|
|
194
|
+
const userId = await hs.auth.consumeLoginToken(body.token).catch(async (err) => {
|
|
195
|
+
await hs.audit.record({
|
|
196
|
+
action: "login_failed",
|
|
197
|
+
actor: null,
|
|
198
|
+
ip: ctx.ip,
|
|
199
|
+
outcome: "failure",
|
|
200
|
+
details: { type: "m.login.token" }
|
|
201
|
+
});
|
|
202
|
+
throw err;
|
|
203
|
+
});
|
|
204
|
+
const user = await hs.db.findOne("users", [{
|
|
205
|
+
field: "user_id",
|
|
206
|
+
value: userId
|
|
207
|
+
}]);
|
|
208
|
+
if (!user || user.deactivated) throw errors.userDeactivated();
|
|
209
|
+
const session = await hs.auth.createSession(userId, {
|
|
210
|
+
deviceId: body.device_id,
|
|
211
|
+
displayName: body.initial_device_display_name,
|
|
212
|
+
refreshToken: body.refresh_token === true
|
|
213
|
+
});
|
|
214
|
+
await hs.audit.record({
|
|
215
|
+
action: "login",
|
|
216
|
+
actor: userId,
|
|
217
|
+
target: session.device_id,
|
|
218
|
+
ip: ctx.ip,
|
|
219
|
+
userAgent: ctx.request.headers.get("user-agent"),
|
|
220
|
+
details: { type: "m.login.token" }
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
...session,
|
|
224
|
+
home_server: hs.serverName,
|
|
225
|
+
well_known: wellKnown(hs) ?? void 0
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (body.type !== "m.login.password" || !hs.userAuth.passwordLoginEnabled) throw errors.unknown("Unsupported login type");
|
|
229
|
+
const userId = await resolveLoginUser(hs, body);
|
|
230
|
+
if (body.password === void 0) throw errors.missingParam("Missing password");
|
|
231
|
+
let user = await hs.db.findOne("users", [{
|
|
232
|
+
field: "user_id",
|
|
233
|
+
value: userId
|
|
234
|
+
}]);
|
|
235
|
+
const ok = await hs.userAuth.checkPassword({
|
|
236
|
+
userId,
|
|
237
|
+
localpart: userId.slice(1).split(":")[0],
|
|
238
|
+
password: body.password,
|
|
239
|
+
storedHash: user?.password_hash ?? null
|
|
240
|
+
});
|
|
241
|
+
const ua = ctx.request.headers.get("user-agent");
|
|
242
|
+
if (!ok) {
|
|
243
|
+
await hs.audit.record({
|
|
244
|
+
action: "login_failed",
|
|
245
|
+
actor: userId,
|
|
246
|
+
ip: ctx.ip,
|
|
247
|
+
userAgent: ua,
|
|
248
|
+
outcome: "failure",
|
|
249
|
+
details: { type: "m.login.password" }
|
|
250
|
+
});
|
|
251
|
+
throw errors.forbidden("Invalid username or password");
|
|
252
|
+
}
|
|
253
|
+
if (!user) {
|
|
254
|
+
if (!await hs.userAuth.autoProvision(userId)) throw errors.forbidden("Invalid username or password");
|
|
255
|
+
await createUser(hs, userId.slice(1).split(":")[0], {});
|
|
256
|
+
user = await hs.db.findOne("users", [{
|
|
257
|
+
field: "user_id",
|
|
258
|
+
value: userId
|
|
259
|
+
}]);
|
|
260
|
+
}
|
|
261
|
+
if (user.deactivated) throw errors.userDeactivated();
|
|
262
|
+
const session = await hs.auth.createSession(userId, {
|
|
263
|
+
deviceId: body.device_id,
|
|
264
|
+
displayName: body.initial_device_display_name,
|
|
265
|
+
refreshToken: body.refresh_token === true
|
|
266
|
+
});
|
|
267
|
+
await hs.audit.record({
|
|
268
|
+
action: "login",
|
|
269
|
+
actor: userId,
|
|
270
|
+
target: session.device_id,
|
|
271
|
+
ip: ctx.ip,
|
|
272
|
+
userAgent: ua,
|
|
273
|
+
details: { type: "m.login.password" }
|
|
274
|
+
});
|
|
275
|
+
return {
|
|
276
|
+
...session,
|
|
277
|
+
home_server: hs.serverName,
|
|
278
|
+
well_known: wellKnown(hs) ?? void 0
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
r.client("POST", "/refresh", { input: body({ refresh_token: lenient(z.string()) }) }, async (ctx) => {
|
|
282
|
+
const body = ctx.input;
|
|
283
|
+
if (body.refresh_token === void 0) throw errors.missingParam("Missing refresh_token");
|
|
284
|
+
const { access_token, refresh_token, expires_in_ms } = await ctx.hs.auth.refresh(body.refresh_token);
|
|
285
|
+
return {
|
|
286
|
+
access_token,
|
|
287
|
+
refresh_token,
|
|
288
|
+
expires_in_ms
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
r.client("POST", "/logout", async (ctx) => {
|
|
292
|
+
const s = await ctx.auth();
|
|
293
|
+
await ctx.hs.auth.deleteDevice(s.userId, s.deviceId);
|
|
294
|
+
await ctx.hs.audit.record({
|
|
295
|
+
action: "logout",
|
|
296
|
+
actor: s.userId,
|
|
297
|
+
target: s.deviceId,
|
|
298
|
+
ip: ctx.ip
|
|
299
|
+
});
|
|
300
|
+
return {};
|
|
301
|
+
});
|
|
302
|
+
r.client("POST", "/logout/all", async (ctx) => {
|
|
303
|
+
const s = await ctx.auth();
|
|
304
|
+
await ctx.hs.auth.deleteAllDevices(s.userId);
|
|
305
|
+
await ctx.hs.audit.record({
|
|
306
|
+
action: "logout",
|
|
307
|
+
actor: s.userId,
|
|
308
|
+
ip: ctx.ip,
|
|
309
|
+
details: { all: true }
|
|
310
|
+
});
|
|
311
|
+
return {};
|
|
312
|
+
});
|
|
313
|
+
r.client("GET", "/account/whoami", async (ctx) => {
|
|
314
|
+
const s = await ctx.auth();
|
|
315
|
+
return {
|
|
316
|
+
user_id: s.userId,
|
|
317
|
+
...s.deviceId ? { device_id: s.deviceId } : {},
|
|
318
|
+
is_guest: s.isGuest
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
r.client("GET", "/register/available", async (ctx) => {
|
|
322
|
+
const username = ctx.query.get("username");
|
|
323
|
+
if (!username) throw errors.missingParam("Missing username");
|
|
324
|
+
await validateNewUsername(ctx.hs, username);
|
|
325
|
+
return { available: true };
|
|
326
|
+
});
|
|
327
|
+
r.client("GET", "/register", async (ctx) => ({
|
|
328
|
+
flows: (await registrationFlows(ctx.hs)).map((stages) => ({ stages })),
|
|
329
|
+
params: {}
|
|
330
|
+
}));
|
|
331
|
+
r.client("GET", "/register/m.login.registration_token/validity", async (ctx) => {
|
|
332
|
+
await ctx.rateLimit("register");
|
|
333
|
+
const token = ctx.query.get("token");
|
|
334
|
+
if (!token) throw errors.missingParam("Missing token");
|
|
335
|
+
return { valid: await registrationTokenValid(ctx.hs, token) };
|
|
336
|
+
}, ["v1"]);
|
|
337
|
+
r.client("POST", "/register", { input: registerBody }, async (ctx) => {
|
|
338
|
+
const hs = ctx.hs;
|
|
339
|
+
const body = ctx.input;
|
|
340
|
+
const kind = ctx.query.get("kind") ?? "user";
|
|
341
|
+
if (kind === "guest") {
|
|
342
|
+
if (!hs.userAuth.guestAccessEnabled) throw errors.forbidden("Guest access is disabled");
|
|
343
|
+
const userId = await createUser(hs, await generateLocalpart(hs), { guest: true });
|
|
344
|
+
return {
|
|
345
|
+
...await hs.auth.createSession(userId, { displayName: body.initial_device_display_name }),
|
|
346
|
+
home_server: hs.serverName
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
if (kind !== "user") throw errors.invalidParam("Unknown kind");
|
|
350
|
+
if (body.type === "m.login.application_service" || body.auth?.type === "m.login.application_service") {
|
|
351
|
+
const s = await ctx.auth();
|
|
352
|
+
if (!s.appservice) throw errors.forbidden("An application service token is required");
|
|
353
|
+
if (body.username === void 0) throw errors.missingParam("Missing username");
|
|
354
|
+
const localpart = body.username.toLowerCase();
|
|
355
|
+
const userId = `@${localpart}:${hs.serverName}`;
|
|
356
|
+
if (!hs.appservices.isInterestedUser(s.appservice, userId)) throw errors.exclusive("User is not in the application service's namespace");
|
|
357
|
+
if (!LOCALPART_RE.test(localpart)) throw errors.invalidUsername();
|
|
358
|
+
if (await hs.db.findOne("users", [{
|
|
359
|
+
field: "user_id",
|
|
360
|
+
value: userId
|
|
361
|
+
}])) throw errors.userInUse();
|
|
362
|
+
await createUser(hs, localpart, {});
|
|
363
|
+
if (body.inhibit_login === true) return {
|
|
364
|
+
user_id: userId,
|
|
365
|
+
home_server: hs.serverName
|
|
366
|
+
};
|
|
367
|
+
return {
|
|
368
|
+
...await hs.auth.createSession(userId, {
|
|
369
|
+
deviceId: body.device_id,
|
|
370
|
+
displayName: body.initial_device_display_name
|
|
371
|
+
}),
|
|
372
|
+
home_server: hs.serverName
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (ctx.accessToken() && hs.appservices.byToken(ctx.accessToken())) throw errors.missingParam("Application services must use type m.login.application_service");
|
|
376
|
+
rejectIfDelegated(hs);
|
|
377
|
+
if (!await hs.registrationAllowed()) throw errors.forbidden("Registration is disabled");
|
|
378
|
+
await ctx.rateLimit("register");
|
|
379
|
+
const localpart = body.username !== void 0 ? await validateNewUsername(hs, body.username.toLowerCase()) : await generateLocalpart(hs);
|
|
380
|
+
if (hs.appservices.exclusiveUserOwner(`@${localpart}:${hs.serverName}`)) throw errors.exclusive("This user ID is reserved by an application service");
|
|
381
|
+
if (body.password !== void 0) {
|
|
382
|
+
const problem = await hs.userAuth.validatePassword(body.password);
|
|
383
|
+
if (problem) throw new MatrixError(400, "M_WEAK_PASSWORD", problem);
|
|
384
|
+
}
|
|
385
|
+
const flows = await registrationFlows(hs);
|
|
386
|
+
const { threepids } = await requireUia(hs, body, flows, null);
|
|
387
|
+
await consumeRegistrationToken(hs, body);
|
|
388
|
+
if (await hs.db.findOne("users", [{
|
|
389
|
+
field: "localpart",
|
|
390
|
+
value: localpart
|
|
391
|
+
}])) throw errors.userInUse();
|
|
392
|
+
const userId = await createUser(hs, localpart, { password: body.password });
|
|
393
|
+
for (const t of threepids) await hs.threepids.add(userId, t).catch((err) => hs.logger.warn("Could not attach", t.medium, "to", userId, err));
|
|
394
|
+
await hs.audit.record({
|
|
395
|
+
action: "register",
|
|
396
|
+
actor: userId,
|
|
397
|
+
ip: ctx.ip,
|
|
398
|
+
userAgent: ctx.request.headers.get("user-agent"),
|
|
399
|
+
details: { token: body.auth?.type === "m.login.registration_token" }
|
|
400
|
+
});
|
|
401
|
+
if (body.inhibit_login === true) return {
|
|
402
|
+
user_id: userId,
|
|
403
|
+
home_server: hs.serverName
|
|
404
|
+
};
|
|
405
|
+
return {
|
|
406
|
+
...await hs.auth.createSession(userId, {
|
|
407
|
+
deviceId: body.device_id,
|
|
408
|
+
displayName: body.initial_device_display_name,
|
|
409
|
+
refreshToken: body.refresh_token === true
|
|
410
|
+
}),
|
|
411
|
+
home_server: hs.serverName
|
|
412
|
+
};
|
|
413
|
+
});
|
|
414
|
+
r.client("POST", "/account/password", { input: passwordBody }, async (ctx) => {
|
|
415
|
+
const hs = ctx.hs;
|
|
416
|
+
rejectIfDelegated(hs);
|
|
417
|
+
const body = ctx.input;
|
|
418
|
+
let s;
|
|
419
|
+
if (ctx.accessToken()) {
|
|
420
|
+
const session = await ctx.auth();
|
|
421
|
+
await requireUia(hs, body, [["m.login.password"]], session.userId);
|
|
422
|
+
s = {
|
|
423
|
+
userId: session.userId,
|
|
424
|
+
deviceId: session.deviceId
|
|
425
|
+
};
|
|
426
|
+
} else {
|
|
427
|
+
const { threepids } = await requireUia(hs, body, [["m.login.email.identity"], ["m.login.msisdn"]], null);
|
|
428
|
+
const owner = threepids.length ? await hs.threepids.userFor(threepids[0].medium, threepids[0].address) : null;
|
|
429
|
+
if (!owner) throw new MatrixError(400, "M_THREEPID_NOT_FOUND", "No account uses this address");
|
|
430
|
+
s = {
|
|
431
|
+
userId: owner,
|
|
432
|
+
deviceId: null
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (!body.new_password) throw errors.missingParam("Missing new_password");
|
|
436
|
+
const problem = await hs.userAuth.validatePassword(body.new_password);
|
|
437
|
+
if (problem) throw new MatrixError(400, "M_WEAK_PASSWORD", problem);
|
|
438
|
+
await hs.db.update("users", [{
|
|
439
|
+
field: "user_id",
|
|
440
|
+
value: s.userId
|
|
441
|
+
}], { password_hash: await hs.userAuth.hashPassword(body.new_password) });
|
|
442
|
+
await hs.audit.record({
|
|
443
|
+
action: "password_change",
|
|
444
|
+
actor: s.userId,
|
|
445
|
+
ip: ctx.ip,
|
|
446
|
+
details: { logout_devices: body.logout_devices !== false }
|
|
447
|
+
});
|
|
448
|
+
if (body.logout_devices !== false) await hs.auth.deleteAllDevices(s.userId, s.deviceId ?? void 0);
|
|
449
|
+
return {};
|
|
450
|
+
});
|
|
451
|
+
r.client("POST", "/account/deactivate", { input: body({}) }, async (ctx) => {
|
|
452
|
+
const hs = ctx.hs;
|
|
453
|
+
rejectIfDelegated(hs);
|
|
454
|
+
const s = await ctx.auth();
|
|
455
|
+
await requireUia(hs, ctx.input, [["m.login.password"]], s.userId);
|
|
456
|
+
for (const membership of [
|
|
457
|
+
"join",
|
|
458
|
+
"invite",
|
|
459
|
+
"knock"
|
|
460
|
+
]) for (const roomId of await hs.rooms.roomsForUser(s.userId, membership)) try {
|
|
461
|
+
await hs.rooms.setMembership(roomId, s.userId, s.userId, "leave");
|
|
462
|
+
} catch {}
|
|
463
|
+
await hs.auth.deleteAllDevices(s.userId);
|
|
464
|
+
await hs.db.update("users", [{
|
|
465
|
+
field: "user_id",
|
|
466
|
+
value: s.userId
|
|
467
|
+
}], {
|
|
468
|
+
deactivated: true,
|
|
469
|
+
password_hash: null,
|
|
470
|
+
displayname: null,
|
|
471
|
+
avatar_url: null
|
|
472
|
+
});
|
|
473
|
+
await hs.db.delete("profile_fields", [{
|
|
474
|
+
field: "user_id",
|
|
475
|
+
value: s.userId
|
|
476
|
+
}]);
|
|
477
|
+
const unbind = await hs.threepids.removeAll(s.userId);
|
|
478
|
+
await hs.audit.record({
|
|
479
|
+
action: "deactivate",
|
|
480
|
+
actor: s.userId,
|
|
481
|
+
target: s.userId,
|
|
482
|
+
ip: ctx.ip
|
|
483
|
+
});
|
|
484
|
+
return { id_server_unbind_result: unbind };
|
|
485
|
+
});
|
|
486
|
+
r.get("/_synapse/admin/v1/register", async (ctx) => {
|
|
487
|
+
if (!ctx.hs.config.registrationSharedSecret) throw errors.unrecognized();
|
|
488
|
+
const nonce = randomString(32);
|
|
489
|
+
await ctx.hs.cluster.set(`regnonce:${nonce}`, true, 6e4);
|
|
490
|
+
return { nonce };
|
|
491
|
+
});
|
|
492
|
+
r.post("/_synapse/admin/v1/register", { input: sharedSecretRegisterBody }, async (ctx) => {
|
|
493
|
+
const hs = ctx.hs;
|
|
494
|
+
const secret = hs.config.registrationSharedSecret;
|
|
495
|
+
if (!secret) throw errors.unrecognized();
|
|
496
|
+
const body = ctx.input;
|
|
497
|
+
if (body.nonce === void 0 || !await hs.cluster.get(`regnonce:${body.nonce}`)) throw errors.unknown("Unrecognised nonce");
|
|
498
|
+
await hs.cluster.delete(`regnonce:${body.nonce}`);
|
|
499
|
+
const admin = body.admin === true;
|
|
500
|
+
const expected = createHmac("sha1", secret).update(`${body.nonce}\u0000${body.username}\u0000${body.password}\u0000${admin ? "admin" : "notadmin"}`).digest("hex");
|
|
501
|
+
const given = Buffer.from(body.mac ?? "", "utf8");
|
|
502
|
+
if (given.length !== expected.length || !timingSafeEqual(given, Buffer.from(expected, "utf8"))) throw errors.forbidden("HMAC incorrect");
|
|
503
|
+
const userId = await createUser(hs, await validateNewUsername(hs, body.username.toLowerCase()), {
|
|
504
|
+
password: body.password,
|
|
505
|
+
admin,
|
|
506
|
+
displayname: body.displayname
|
|
507
|
+
});
|
|
508
|
+
const session = await hs.auth.createSession(userId);
|
|
509
|
+
await hs.audit.record({
|
|
510
|
+
action: "register",
|
|
511
|
+
actor: "server",
|
|
512
|
+
target: userId,
|
|
513
|
+
ip: ctx.ip,
|
|
514
|
+
details: {
|
|
515
|
+
shared_secret: true,
|
|
516
|
+
admin
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
return {
|
|
520
|
+
...session,
|
|
521
|
+
home_server: hs.serverName
|
|
522
|
+
};
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
async function registrationFlows(hs) {
|
|
526
|
+
if (hs.adminRoom?.options.firstUserCanRegister && await hs.adminRoom.noUsersYet()) return [["m.login.dummy"]];
|
|
527
|
+
const base = await hs.registrationMode() === "token" ? ["m.login.registration_token"] : ["m.login.dummy"];
|
|
528
|
+
if (!hs.threepids.canValidate("email")) return [base];
|
|
529
|
+
const withEmail = [...base.filter((s) => s !== "m.login.dummy"), "m.login.email.identity"];
|
|
530
|
+
return hs.config.threepid.requireEmailForRegistration ? [withEmail] : [base, withEmail];
|
|
531
|
+
}
|
|
532
|
+
async function registrationTokenValid(hs, token) {
|
|
533
|
+
const row = await hs.db.findOne("registration_tokens", [{
|
|
534
|
+
field: "token",
|
|
535
|
+
value: token
|
|
536
|
+
}]);
|
|
537
|
+
if (!row) return false;
|
|
538
|
+
if (row.expiry_time !== null && row.expiry_time < Date.now()) return false;
|
|
539
|
+
return row.uses_allowed === null || row.completed < row.uses_allowed;
|
|
540
|
+
}
|
|
541
|
+
async function consumeRegistrationToken(hs, body) {
|
|
542
|
+
if (body.auth?.type !== "m.login.registration_token") return;
|
|
543
|
+
const token = body.auth?.token;
|
|
544
|
+
const row = await hs.db.findOne("registration_tokens", [{
|
|
545
|
+
field: "token",
|
|
546
|
+
value: token
|
|
547
|
+
}]);
|
|
548
|
+
if (row) await hs.db.update("registration_tokens", [{
|
|
549
|
+
field: "token",
|
|
550
|
+
value: token
|
|
551
|
+
}], { completed: row.completed + 1 });
|
|
552
|
+
}
|
|
553
|
+
function wellKnown(hs) {
|
|
554
|
+
if (!hs.config.publicBaseUrl) return null;
|
|
555
|
+
const wk = { "m.homeserver": { base_url: hs.config.publicBaseUrl } };
|
|
556
|
+
if (hs.config.rtc.transports?.length) wk["org.matrix.msc4143.rtc_foci"] = hs.config.rtc.transports;
|
|
557
|
+
return wk;
|
|
558
|
+
}
|
|
559
|
+
//#endregion
|
|
560
|
+
export { createUser, registerAccountRoutes, registrationFlows, registrationTokenValid, wellKnown };
|