Implement MSC1961

See https://github.com/matrix-org/matrix-doc/pull/1961
This commit is contained in:
Travis Ralston 2019-06-27 21:46:00 -06:00
parent d021974a22
commit 57d585d68a
17 changed files with 327 additions and 121 deletions

9
package-lock.json generated
View File

@ -11705,6 +11705,15 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.3.tgz",
"integrity": "sha512-FFgHdPt4T/duxx6Ndf7hwgMZZjZpB+U0nMNGVCYPq0rEzWKjEDobm4J6yb3CS7naZ0yURFqdw9Gwc7UOh/P9oQ=="
},
"typescript-ioc": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/typescript-ioc/-/typescript-ioc-1.2.5.tgz",
"integrity": "sha512-HErBOZfOmrJ9N8QZDOHvP56FqvqZJMkaFW9Qm4ExQa93ilUnhQ+S7n80rVfUQPceZWIImsEBU/Kt19W5KXBDEw==",
"requires": {
"reflect-metadata": "^0.1.10",
"require-glob": "^3.2.0"
}
},
"typescript-rest": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/typescript-rest/-/typescript-rest-2.0.0.tgz",

View File

@ -49,6 +49,7 @@
"sqlite3": "^4.0.6",
"telegraf": "^3.28.0",
"typescript": "^3.4.3",
"typescript-ioc": "^1.2.5",
"typescript-rest": "^2.0.0",
"umzug": "^2.2.0",
"url": "^0.11.0"

View File

@ -7,6 +7,7 @@ import { Server } from "typescript-rest";
import * as _ from "lodash";
import config from "../config";
import { ApiError } from "./ApiError";
import MSCSecurity from "./security/MSCSecurity";
/**
* Web server for Dimension. Handles the API routes for the admin, scalar, dimension, and matrix APIs.
@ -23,8 +24,12 @@ export default class Webserver {
}
private loadRoutes() {
const apis = ["scalar", "dimension", "admin", "matrix"].map(a => path.join(__dirname, a, "*.js"));
// TODO: Rename services to controllers, and controllers to services. They're backwards.
const apis = ["scalar", "dimension", "admin", "matrix", "msc"].map(a => path.join(__dirname, a, "*.js"));
const router = express.Router();
Server.useIoC();
Server.registerAuthenticator(new MSCSecurity());
apis.forEach(a => Server.loadServices(router, [a]));
const routes = _.uniq(router.stack.map(r => r.route.path));
for (const route of routes) {

View File

@ -1,5 +1,4 @@
import { GET, Path, POST, QueryParam } from "typescript-rest";
import { ScalarService } from "../scalar/ScalarService";
import config from "../../config";
import { ApiError } from "../ApiError";
import { MatrixLiteClient } from "../../matrix/MatrixLiteClient";
@ -7,6 +6,7 @@ import { CURRENT_VERSION } from "../../version";
import { getFederationConnInfo } from "../../matrix/helpers";
import UserScalarToken from "../../db/models/UserScalarToken";
import { Cache, CACHE_SCALAR_ACCOUNTS } from "../../MemoryCache";
import AccountController from "../controllers/AccountController";
interface DimensionVersionResponse {
version: string;
@ -50,7 +50,8 @@ export class AdminService {
* @throws {ApiError} Thrown with a status code of 401 if the owner is not an administrator
*/
public static async validateAndGetAdminTokenOwner(scalarToken: string): Promise<string> {
const userId = await ScalarService.getTokenOwner(scalarToken, true);
const accountController = new AccountController();
const userId = await accountController.getTokenOwner(scalarToken, true);
if (!AdminService.isAdmin(userId))
throw new ApiError(401, "You must be an administrator to use this API");
return userId;

View File

@ -0,0 +1,123 @@
import { OpenId } from "../../models/OpenId";
import { MatrixOpenIdClient } from "../../matrix/MatrixOpenIdClient";
import { LogService } from "matrix-js-snippets";
import { ApiError } from "../ApiError";
import User from "../../db/models/User";
import Upstream from "../../db/models/Upstream";
import { ScalarStore } from "../../db/ScalarStore";
import UserScalarToken from "../../db/models/UserScalarToken";
import { ScalarClient } from "../../scalar/ScalarClient";
import * as randomString from "random-string";
import { AutoWired } from "typescript-ioc/es6";
import { Cache, CACHE_SCALAR_ACCOUNTS } from "../../MemoryCache";
import { IMSCUser } from "../security/MSCSecurity";
export interface IAccountRegisteredResponse {
token: string;
}
export interface IAccountInfoResponse {
user_id: string;
}
/**
* API controller for account management
*/
@AutoWired
export default class AccountController {
constructor() {
}
/**
* Gets the owner of a given scalar token, throwing an ApiError if the token is invalid.
* @param {string} scalarToken The scalar token to validate
* @param {boolean} ignoreUpstreams True to consider the token valid if it is missing links to other upstreams
* @returns {Promise<string>} Resolves to the owner's user ID if the token is valid.
* @throws {ApiError} Thrown with a status code of 401 if the token is invalid.
*/
public async getTokenOwner(scalarToken: string, ignoreUpstreams = false): Promise<string> {
const cachedUserId = Cache.for(CACHE_SCALAR_ACCOUNTS).get(scalarToken);
if (cachedUserId) return cachedUserId;
try {
const user = await ScalarStore.getTokenOwner(scalarToken, ignoreUpstreams);
Cache.for(CACHE_SCALAR_ACCOUNTS).put(scalarToken, user.userId, 30 * 60 * 1000); // 30 minutes
return user.userId;
} catch (err) {
LogService.error("ScalarService", err);
throw new ApiError(401, "Invalid token");
}
}
/**
* Registers an account to use the Integration Manager
* @param {OpenId} openId The OpenID request information.
* @returns {Promise<IAccountRegisteredResponse>} Resolves when registered.
*/
public async registerAccount(openId: OpenId): Promise<IAccountRegisteredResponse> {
if (!openId || !openId.matrix_server_name || !openId.access_token) {
throw new ApiError(400, "Missing OpenID information");
}
const mxClient = new MatrixOpenIdClient(openId);
const mxUserId = await mxClient.getUserId();
if (!mxUserId.endsWith(":" + openId.matrix_server_name)) {
LogService.warn("AccountController", `OpenID subject '${mxUserId}' does not belong to the homeserver '${openId.matrix_server_name}'`);
throw new ApiError(401, "Invalid token");
}
const user = await User.findByPrimary(mxUserId);
if (!user) {
// There's a small chance we'll get a validation error because of:
// https://github.com/vector-im/riot-web/issues/5846
LogService.verbose("AccountController", "User " + mxUserId + " never seen before - creating");
await User.create({userId: mxUserId});
}
const upstreams = await Upstream.findAll();
await Promise.all(upstreams.map(async upstream => {
if (!await ScalarStore.isUpstreamOnline(upstream)) {
LogService.warn("AccountController", `Skipping registration for ${mxUserId} on upstream ${upstream.id} (${upstream.name}) because it is offline`);
return null;
}
const tokens = await UserScalarToken.findAll({where: {userId: mxUserId, upstreamId: upstream.id}});
if (!tokens || tokens.length === 0) {
LogService.info("AccountController", "Registering " + mxUserId + " for a token at upstream " + upstream.id + " (" + upstream.name + ")");
const client = new ScalarClient(upstream);
const response = await client.register(openId);
return UserScalarToken.create({
userId: mxUserId,
scalarToken: response.scalar_token,
isDimensionToken: false,
upstreamId: upstream.id,
});
}
}).filter(token => !!token));
const dimensionToken = randomString({length: 25});
const dimensionScalarToken = await UserScalarToken.create({
userId: mxUserId,
scalarToken: dimensionToken,
isDimensionToken: true,
});
LogService.info("AccountController", mxUserId + " has registered for a scalar token successfully");
return {token: dimensionScalarToken.scalarToken};
}
/**
* Logs a user out
* @param {IMSCUser} user The user to log out
* @returns {Promise<*>} Resolves when complete.
*/
public async logout(user: IMSCUser): Promise<any> {
// TODO: Create a link to upstream tokens to log them out too
const tokens = await UserScalarToken.findAll({where: {scalarToken: user.token}});
for (const token of tokens) {
await token.destroy();
}
Cache.for(CACHE_SCALAR_ACCOUNTS).clear();
return {};
}
}

View File

@ -1,8 +1,9 @@
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
import { ScalarService } from "../scalar/ScalarService";
import { ApiError } from "../ApiError";
import { BridgedRoom, GitterBridge } from "../../bridges/GitterBridge";
import { LogService } from "matrix-js-snippets";
import { AutoWired, Inject } from "typescript-ioc/es6";
import AccountController from "../controllers/AccountController";
interface BridgeRoomRequest {
gitterRoomName: string;
@ -12,12 +13,16 @@ interface BridgeRoomRequest {
* API for interacting with the Gitter bridge
*/
@Path("/api/v1/dimension/gitter")
@AutoWired
export class DimensionGitterService {
@Inject
private accountController: AccountController;
@GET
@Path("room/:roomId/link")
public async getLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<BridgedRoom> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
try {
const gitter = new GitterBridge(userId);
@ -31,7 +36,7 @@ export class DimensionGitterService {
@POST
@Path("room/:roomId/link")
public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<BridgedRoom> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
try {
const gitter = new GitterBridge(userId);
@ -46,7 +51,7 @@ export class DimensionGitterService {
@DELETE
@Path("room/:roomId/link")
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<any> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
try {
const gitter = new GitterBridge(userId);

View File

@ -1,5 +1,4 @@
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
import { ScalarService } from "../scalar/ScalarService";
import { Widget } from "../../integrations/Widget";
import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache";
import { Integration } from "../../integrations/Integration";
@ -11,6 +10,8 @@ import { ComplexBot } from "../../integrations/ComplexBot";
import { Bridge } from "../../integrations/Bridge";
import { BridgeStore } from "../../db/BridgeStore";
import { BotStore } from "../../db/BotStore";
import AccountController from "../controllers/AccountController";
import { AutoWired, Inject } from "typescript-ioc/es6";
export interface IntegrationsResponse {
widgets: Widget[],
@ -23,8 +24,12 @@ export interface IntegrationsResponse {
* API for managing integrations, primarily for a given room
*/
@Path("/api/v1/dimension/integrations")
@AutoWired
export class DimensionIntegrationsService {
@Inject
private accountController: AccountController;
/**
* Gets a list of widgets
* @param {boolean} enabledOnly True to only return the enabled widgets
@ -86,7 +91,7 @@ export class DimensionIntegrationsService {
@GET
@Path("room/:roomId")
public async getIntegrationsInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<IntegrationsResponse> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
return {
widgets: await DimensionIntegrationsService.getWidgets(true),
bots: await DimensionIntegrationsService.getSimpleBots(userId),
@ -110,7 +115,7 @@ export class DimensionIntegrationsService {
@POST
@Path("room/:roomId/integrations/:category/:type/config")
public async setIntegrationConfigurationInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string, newConfig: any): Promise<any> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
if (category === "complex-bot") await NebStore.setComplexBotConfig(userId, integrationType, roomId, newConfig);
else if (category === "bridge") await BridgeStore.setBridgeRoomConfig(userId, integrationType, roomId, newConfig);
@ -123,7 +128,7 @@ export class DimensionIntegrationsService {
@DELETE
@Path("room/:roomId/integrations/:category/:type")
public async removeIntegrationInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string): Promise<any> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
if (category === "widget") throw new ApiError(400, "Widgets should be removed client-side");
else if (category === "bot") {

View File

@ -1,9 +1,10 @@
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
import { LogService } from "matrix-js-snippets";
import { ScalarService } from "../scalar/ScalarService";
import { IrcBridge } from "../../bridges/IrcBridge";
import IrcBridgeRecord from "../../db/models/IrcBridgeRecord";
import { ApiError } from "../ApiError";
import AccountController from "../controllers/AccountController";
import { AutoWired, Inject } from "typescript-ioc/es6";
interface RequestLinkRequest {
op: string;
@ -13,12 +14,16 @@ interface RequestLinkRequest {
* API for interacting with the IRC bridge
*/
@Path("/api/v1/dimension/irc")
@AutoWired
export class DimensionIrcService {
@Inject
private accountController: AccountController;
@GET
@Path(":networkId/channel/:channel/ops")
public async getOps(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string): Promise<string[]> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
const parsed = IrcBridge.parseNetworkId(networkId);
const bridge = await IrcBridgeRecord.findByPrimary(parsed.bridgeId);
@ -34,7 +39,7 @@ export class DimensionIrcService {
@POST
@Path(":networkId/channel/:channel/link/:roomId")
public async requestLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string, @PathParam("roomId") roomId: string, request: RequestLinkRequest): Promise<any> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
const parsed = IrcBridge.parseNetworkId(networkId);
const bridge = await IrcBridgeRecord.findByPrimary(parsed.bridgeId);
@ -50,7 +55,7 @@ export class DimensionIrcService {
@POST
@Path(":networkId/channel/:channel/unlink/:roomId")
public async unlink(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string, @PathParam("roomId") roomId: string): Promise<any> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
const parsed = IrcBridge.parseNetworkId(networkId);
const bridge = await IrcBridgeRecord.findByPrimary(parsed.bridgeId);

View File

@ -1,9 +1,10 @@
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
import { ScalarService } from "../scalar/ScalarService";
import { ApiError } from "../ApiError";
import { LogService } from "matrix-js-snippets";
import { BridgedChannel, SlackBridge } from "../../bridges/SlackBridge";
import { SlackChannel, SlackTeam } from "../../bridges/models/slack";
import { AutoWired, Inject } from "typescript-ioc/es6";
import AccountController from "../controllers/AccountController";
interface BridgeRoomRequest {
teamId: string;
@ -14,12 +15,16 @@ interface BridgeRoomRequest {
* API for interacting with the Slack bridge
*/
@Path("/api/v1/dimension/slack")
@AutoWired
export class DimensionSlackService {
@Inject
private accountController: AccountController;
@GET
@Path("room/:roomId/link")
public async getLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<BridgedChannel> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
try {
const slack = new SlackBridge(userId);
@ -33,7 +38,7 @@ export class DimensionSlackService {
@POST
@Path("room/:roomId/link")
public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<BridgedChannel> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
try {
const slack = new SlackBridge(userId);
@ -48,7 +53,7 @@ export class DimensionSlackService {
@DELETE
@Path("room/:roomId/link")
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<any> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
try {
const slack = new SlackBridge(userId);
@ -65,7 +70,7 @@ export class DimensionSlackService {
@GET
@Path("teams")
public async getTeams(@QueryParam("scalar_token") scalarToken: string): Promise<SlackTeam[]> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
const slack = new SlackBridge(userId);
const teams = await slack.getTeams();
@ -76,7 +81,7 @@ export class DimensionSlackService {
@GET
@Path("teams/:teamId/channels")
public async getChannels(@QueryParam("scalar_token") scalarToken: string, @PathParam("teamId") teamId: string): Promise<SlackChannel[]> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
try {
const slack = new SlackBridge(userId);
@ -90,7 +95,7 @@ export class DimensionSlackService {
@GET
@Path("auth")
public async getAuthUrl(@QueryParam("scalar_token") scalarToken: string): Promise<{ authUrl: string }> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
try {
const slack = new SlackBridge(userId);

View File

@ -2,12 +2,13 @@ import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
import { Cache, CACHE_STICKERS } from "../../MemoryCache";
import StickerPack from "../../db/models/StickerPack";
import Sticker from "../../db/models/Sticker";
import { ScalarService } from "../scalar/ScalarService";
import UserStickerPack from "../../db/models/UserStickerPack";
import { ApiError } from "../ApiError";
import { StickerpackMetadataDownloader } from "../../utils/StickerpackMetadataDownloader";
import { MatrixStickerBot } from "../../matrix/MatrixStickerBot";
import config from "../../config";
import { AutoWired, Inject } from "typescript-ioc/es6";
import AccountController from "../controllers/AccountController";
export interface MemoryStickerPack {
id: number;
@ -63,8 +64,12 @@ interface StickerConfig {
* API for stickers
*/
@Path("/api/v1/dimension/stickers")
@AutoWired
export class DimensionStickerService {
@Inject
private accountController: AccountController;
public static async getStickerPacks(enabledOnly = false): Promise<MemoryStickerPack[]> {
const cachedPacks = Cache.for(CACHE_STICKERS).get("packs");
if (cachedPacks) {
@ -86,7 +91,7 @@ export class DimensionStickerService {
@GET
@Path("config")
public async getConfig(@QueryParam("scalar_token") scalarToken: string): Promise<StickerConfig> {
await ScalarService.getTokenOwner(scalarToken);
await this.accountController.getTokenOwner(scalarToken);
return {
enabled: config.stickers.enabled,
@ -98,7 +103,7 @@ export class DimensionStickerService {
@GET
@Path("packs")
public async getStickerPacks(@QueryParam("scalar_token") scalarToken: string): Promise<MemoryStickerPack[]> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
const cachedPacks = Cache.for(CACHE_STICKERS).get("packs_" + userId);
if (cachedPacks) return cachedPacks;
@ -125,7 +130,7 @@ export class DimensionStickerService {
@POST
@Path("packs/:packId/selected")
public async setPackSelected(@QueryParam("scalar_token") scalarToken: string, @PathParam("packId") packId: number, request: SetSelectedRequest): Promise<any> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
const pack = await StickerPack.findByPrimary(packId);
if (!pack) throw new ApiError(404, "Sticker pack not found");
@ -149,7 +154,7 @@ export class DimensionStickerService {
@POST
@Path("packs/import")
public async importPack(@QueryParam("scalar_token") scalarToken: string, request: ImportPackRequest): Promise<MemoryUserStickerPack> {
await ScalarService.getTokenOwner(scalarToken);
await this.accountController.getTokenOwner(scalarToken);
if (!config.stickers.enabled) {
throw new ApiError(400, "Custom stickerpacks are disabled on this homeserver");

View File

@ -1,7 +1,8 @@
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
import { ScalarService } from "../scalar/ScalarService";
import { TelegramBridge } from "../../bridges/TelegramBridge";
import { ApiError } from "../ApiError";
import { AutoWired, Inject } from "typescript-ioc/es6";
import AccountController from "../controllers/AccountController";
interface PortalInfoResponse {
bridged: boolean;
@ -19,12 +20,16 @@ interface BridgeRoomRequest {
* API for interacting with the Telegram bridge
*/
@Path("/api/v1/dimension/telegram")
@AutoWired
export class DimensionTelegramService {
@Inject
private accountController: AccountController;
@GET
@Path("chat/:chatId")
public async getPortalInfo(@QueryParam("scalar_token") scalarToken: string, @PathParam("chatId") chatId: number, @QueryParam("roomId") roomId: string): Promise<PortalInfoResponse> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
try {
const telegram = new TelegramBridge(userId);
@ -47,7 +52,7 @@ export class DimensionTelegramService {
@POST
@Path("chat/:chatId/room/:roomId")
public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("chatId") chatId: number, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<PortalInfoResponse> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
try {
const telegram = new TelegramBridge(userId);
@ -69,7 +74,7 @@ export class DimensionTelegramService {
@DELETE
@Path("room/:roomId")
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<PortalInfoResponse> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
try {
const telegram = new TelegramBridge(userId);

View File

@ -1,19 +1,24 @@
import { DELETE, FormParam, HeaderParam, Path, PathParam, POST, QueryParam } from "typescript-rest";
import { ScalarService } from "../scalar/ScalarService";
import { SuccessResponse, WebhookConfiguration, WebhookOptions } from "../../bridges/models/webhooks";
import { WebhooksBridge } from "../../bridges/WebhooksBridge";
import Webhook from "../../db/models/Webhook";
import { ApiError } from "../ApiError";
import { LogService } from "matrix-js-snippets";
import * as request from "request";
import { AutoWired, Inject } from "typescript-ioc/es6";
import AccountController from "../controllers/AccountController";
/**
* API for interacting with the Webhooks bridge, and for setting up proxies to other
* services.
*/
@Path("/api/v1/dimension/webhooks")
@AutoWired
export class DimensionWebhooksService {
@Inject
private accountController: AccountController;
@POST
@Path("/travisci/:webhookId")
public async postTravisCiWebhook(@PathParam("webhookId") webhookId: string, @FormParam("payload") payload: string, @HeaderParam("Signature") signature: string): Promise<any> {
@ -43,7 +48,7 @@ export class DimensionWebhooksService {
@POST
@Path("room/:roomId/webhooks/new")
public async newWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, options: WebhookOptions): Promise<WebhookConfiguration> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
const webhooks = new WebhooksBridge(userId);
return webhooks.createWebhook(roomId, options);
@ -52,7 +57,7 @@ export class DimensionWebhooksService {
@POST
@Path("room/:roomId/webhooks/:hookId")
public async updateWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("hookId") hookId: string, options: WebhookOptions): Promise<WebhookConfiguration> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
const webhooks = new WebhooksBridge(userId);
return webhooks.updateWebhook(roomId, hookId, options);
@ -61,7 +66,7 @@ export class DimensionWebhooksService {
@DELETE
@Path("room/:roomId/webhooks/:hookId")
public async deleteWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("hookId") hookId: string): Promise<SuccessResponse> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
const webhooks = new WebhooksBridge(userId);
return webhooks.deleteWebhook(roomId, hookId);

View File

@ -0,0 +1,41 @@
import { Context, GET, Path, POST, Security, ServiceContext } from "typescript-rest";
import { OpenId } from "../../models/OpenId";
import AccountController, { IAccountInfoResponse, IAccountRegisteredResponse } from "../controllers/AccountController";
import { AutoWired, Inject } from "typescript-ioc/es6";
import { IMSCUser, ROLE_MSC_USER } from "../security/MSCSecurity";
/**
* API for account management
*/
@Path("/_matrix/integrations/v1/account")
@AutoWired
export class MSCAccountService {
@Inject
private accountController: AccountController;
@Context
private context: ServiceContext;
@POST
@Path("register")
public async register(request: OpenId): Promise<IAccountRegisteredResponse> {
return this.accountController.registerAccount(request);
}
@GET
@Path("")
@Security(ROLE_MSC_USER)
public async info(): Promise<IAccountInfoResponse> {
const user: IMSCUser = this.context.request.user;
return {user_id: user.userId};
}
@POST
@Path("logout")
@Security(ROLE_MSC_USER)
public async logout(): Promise<any> {
await this.accountController.logout(this.context.request.user);
return {};
}
}

View File

@ -1,104 +1,30 @@
import { GET, Path, POST, QueryParam } from "typescript-rest";
import { MatrixOpenIdClient } from "../../matrix/MatrixOpenIdClient";
import Upstream from "../../db/models/Upstream";
import { ScalarClient } from "../../scalar/ScalarClient";
import User from "../../db/models/User";
import UserScalarToken from "../../db/models/UserScalarToken";
import { LogService } from "matrix-js-snippets";
import { ApiError } from "../ApiError";
import * as randomString from "random-string";
import { OpenId } from "../../models/OpenId";
import { ScalarAccountResponse, ScalarRegisterResponse } from "../../models/ScalarResponses";
import { Cache, CACHE_SCALAR_ACCOUNTS } from "../../MemoryCache";
import { ScalarStore } from "../../db/ScalarStore";
interface RegisterRequest {
access_token: string;
token_type: string;
matrix_server_name: string;
expires_in: number;
}
import { AutoWired, Inject } from "typescript-ioc/es6";
import AccountController from "../controllers/AccountController";
/**
* API for the minimum Scalar API we need to implement to be compatible with clients. Used for registration
* and general account management.
*/
@Path("/api/v1/scalar")
@AutoWired
export class ScalarService {
/**
* Gets the owner of a given scalar token, throwing an ApiError if the token is invalid.
* @param {string} scalarToken The scalar token to validate
* @param {boolean} ignoreUpstreams True to consider the token valid if it is missing links to other upstreams
* @returns {Promise<string>} Resolves to the owner's user ID if the token is valid.
* @throws {ApiError} Thrown with a status code of 401 if the token is invalid.
*/
public static async getTokenOwner(scalarToken: string, ignoreUpstreams = false): Promise<string> {
const cachedUserId = Cache.for(CACHE_SCALAR_ACCOUNTS).get(scalarToken);
if (cachedUserId) return cachedUserId;
try {
const user = await ScalarStore.getTokenOwner(scalarToken, ignoreUpstreams);
Cache.for(CACHE_SCALAR_ACCOUNTS).put(scalarToken, user.userId, 30 * 60 * 1000); // 30 minutes
return user.userId;
} catch (err) {
LogService.error("ScalarService", err);
throw new ApiError(401, "Invalid token");
}
}
@Inject
private accountController: AccountController;
@POST
@Path("register")
public async register(request: RegisterRequest, @QueryParam("v") apiVersion: string): Promise<ScalarRegisterResponse> {
public async register(request: OpenId, @QueryParam("v") apiVersion: string): Promise<ScalarRegisterResponse> {
if (apiVersion !== "1.1") {
throw new ApiError(401, "Invalid API version.");
}
const mxClient = new MatrixOpenIdClient(<OpenId>request);
const mxUserId = await mxClient.getUserId();
if (!mxUserId.endsWith(":" + request.matrix_server_name)) {
LogService.warn("ScalarService", `OpenID subject '${mxUserId}' does not belong to the homeserver '${request.matrix_server_name}'`);
throw new ApiError(401, "Invalid token");
}
const user = await User.findByPrimary(mxUserId);
if (!user) {
// There's a small chance we'll get a validation error because of:
// https://github.com/vector-im/riot-web/issues/5846
LogService.verbose("ScalarService", "User " + mxUserId + " never seen before - creating");
await User.create({userId: mxUserId});
}
const upstreams = await Upstream.findAll();
await Promise.all(upstreams.map(async upstream => {
if (!await ScalarStore.isUpstreamOnline(upstream)) {
LogService.warn("ScalarService", `Skipping registration for ${mxUserId} on upstream ${upstream.id} (${upstream.name}) because it is offline`);
return null;
}
const tokens = await UserScalarToken.findAll({where: {userId: mxUserId, upstreamId: upstream.id}});
if (!tokens || tokens.length === 0) {
LogService.info("ScalarService", "Registering " + mxUserId + " for a token at upstream " + upstream.id + " (" + upstream.name + ")");
const client = new ScalarClient(upstream);
const response = await client.register(<OpenId>request);
return UserScalarToken.create({
userId: mxUserId,
scalarToken: response.scalar_token,
isDimensionToken: false,
upstreamId: upstream.id,
});
}
}).filter(token => !!token));
const dimensionToken = randomString({length: 25});
const dimensionScalarToken = await UserScalarToken.create({
userId: mxUserId,
scalarToken: dimensionToken,
isDimensionToken: true,
});
LogService.info("ScalarService", mxUserId + " has registered for a scalar token successfully");
return {scalar_token: dimensionScalarToken.scalarToken};
const response = await this.accountController.registerAccount(request);
return {scalar_token: response.token};
}
@GET
@ -108,7 +34,7 @@ export class ScalarService {
throw new ApiError(401, "Invalid API version.");
}
const userId = await ScalarService.getTokenOwner(scalarToken);
const userId = await this.accountController.getTokenOwner(scalarToken);
return {user_id: userId};
}

View File

@ -3,8 +3,9 @@ import { LogService } from "matrix-js-snippets";
import { Cache, CACHE_WIDGET_TITLES } from "../../MemoryCache";
import { MatrixLiteClient } from "../../matrix/MatrixLiteClient";
import config from "../../config";
import { ScalarService } from "./ScalarService";
import moment = require("moment");
import { AutoWired, Inject } from "typescript-ioc/es6";
import AccountController from "../controllers/AccountController";
interface UrlPreviewResponse {
cached_response: boolean;
@ -22,12 +23,16 @@ interface UrlPreviewResponse {
* API for the minimum Scalar API for widget functionality in clients.
*/
@Path("/api/v1/scalar/widgets")
@AutoWired
export class ScalarWidgetService {
@Inject
private accountController: AccountController;
@GET
@Path("title_lookup")
public async titleLookup(@QueryParam("scalar_token") scalarToken: string, @QueryParam("curl") url: string): Promise<UrlPreviewResponse> {
await ScalarService.getTokenOwner(scalarToken);
await this.accountController.getTokenOwner(scalarToken);
const cachedResult = Cache.for(CACHE_WIDGET_TITLES).get(url);
if (cachedResult) {

View File

@ -0,0 +1,56 @@
import { ServiceAuthenticator } from "typescript-rest";
import { Request, RequestHandler, Response, Router } from "express";
import { ApiError } from "../ApiError";
import { LogService } from "matrix-js-snippets";
import AccountController from "../controllers/AccountController";
export interface IMSCUser {
userId: string;
token: string;
}
export const ROLE_MSC_USER = "ROLE_MSC_USER";
export default class MSCSecurity implements ServiceAuthenticator {
private accountController = new AccountController();
public getRoles(req: Request): string[] {
if (req.user) return [ROLE_MSC_USER];
return [];
}
getMiddleware(): RequestHandler {
return (async (req: Request, res: Response, next: () => void) => {
try {
if (req.headers.authorization) {
const header = req.headers.authorization;
if (!header.startsWith("Bearer ")) {
return res.status(401).json({errcode: "M_INVALID_TOKEN", error: "Invalid token"});
}
const token = header.substring("Bearer ".length);
req.user = <IMSCUser>{
userId: await this.accountController.getTokenOwner(token),
token: token,
};
return next();
}
console.log(req.query);
} catch (e) {
if (e instanceof ApiError) {
// TODO: Proper error message
res.status(e.statusCode).json({errcode: e.errorCode, error: "Error"});
} else {
LogService.error("MSCSecurity", e);
res.status(500).json({errcode: "M_UNKNOWN", error: "Unknown server error"});
}
}
});
}
initialize(_router: Router): void {
}
}

View File

@ -142,7 +142,11 @@ module.exports = function () {
'/api': {
target: 'http://localhost:8184',
secure: false
}
},
'/_matrix': {
target: 'http://localhost:8184',
secure: false
},
}
};