From 8cd5067e73c842427e6ea22e5fa254e3643b93c8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 13 Apr 2019 16:58:20 -0600 Subject: [PATCH] Assume upstream tokens are valid if they are offline --- src/MemoryCache.ts | 1 + src/db/ScalarStore.ts | 77 ++++++++++++++++++++++++++++++++++ src/matrix/MatrixLiteClient.ts | 9 ++++ src/matrix/MatrixStickerBot.ts | 6 +++ src/scalar/ScalarClient.ts | 24 ++++++++++- 5 files changed, 116 insertions(+), 1 deletion(-) diff --git a/src/MemoryCache.ts b/src/MemoryCache.ts index 2b58dd0..d4e2d74 100644 --- a/src/MemoryCache.ts +++ b/src/MemoryCache.ts @@ -45,6 +45,7 @@ export const CACHE_INTEGRATIONS = "integrations"; export const CACHE_NEB = "neb"; export const CACHE_UPSTREAM = "upstream"; export const CACHE_SCALAR_ACCOUNTS = "scalar-accounts"; +export const CACHE_SCALAR_ONLINE_STATE = "scalar-online-state"; export const CACHE_WIDGET_TITLES = "widget-titles"; export const CACHE_FEDERATION = "federation"; export const CACHE_IRC_BRIDGE = "irc-bridge"; diff --git a/src/db/ScalarStore.ts b/src/db/ScalarStore.ts index 812e2bd..a77437a 100644 --- a/src/db/ScalarStore.ts +++ b/src/db/ScalarStore.ts @@ -2,6 +2,9 @@ import UserScalarToken from "./models/UserScalarToken"; import { LogService } from "matrix-js-snippets"; import Upstream from "./models/Upstream"; import User from "./models/User"; +import { MatrixStickerBot } from "../matrix/MatrixStickerBot"; +import { ScalarClient } from "../scalar/ScalarClient"; +import { CACHE_SCALAR_ONLINE_STATE, Cache } from "../MemoryCache"; export class ScalarStore { @@ -17,6 +20,10 @@ export class ScalarStore { const upstreams = await Upstream.findAll(); for (const upstream of upstreams) { + if (!await ScalarStore.isUpstreamOnline(upstream)) { + LogService.warn("ScalarStore", `Upstream ${upstream.apiUrl} is offline - assuming token is valid`); + continue; + } if (upstreamTokenIds.indexOf(upstream.id) === -1) { LogService.warn("ScalarStore", "user " + userId + " is missing a scalar token for upstream " + upstream.id + " (" + upstream.name + ")"); return false; @@ -43,6 +50,76 @@ export class ScalarStore { return user; } + public static async isUpstreamOnline(upstream: Upstream): Promise { + const cache = Cache.for(CACHE_SCALAR_ONLINE_STATE); + const cacheKey = `Upstream ${upstream.id}`; + const result = cache.get(cacheKey); + if (typeof (result) === 'boolean') { + return result; + } + + const state = ScalarStore.checkIfUpstreamOnline(upstream); + cache.put(cacheKey, state, 60 * 60 * 1000); // 1 hour + return state; + } + + private static async checkIfUpstreamOnline(upstream: Upstream): Promise { + try { + // The sticker bot can be used for this for now + + const testUserId = await MatrixStickerBot.getUserId(); + const scalarClient = new ScalarClient(upstream); + + // First see if we have a token for the upstream so we can try it + const existingTokens = await UserScalarToken.findAll({ + where: {isDimensionToken: false, userId: testUserId, upstreamId: upstream.id}, + }); + if (existingTokens && existingTokens.length) { + // Test that the token works + try { + const result = await scalarClient.getAccount(existingTokens[0].scalarToken); + if (result.user_id !== testUserId) { + // noinspection ExceptionCaughtLocallyJS + throw new Error(`Unexpected error: Upstream ${upstream.id} did not return account info for the right token`); + } + return true; // it's online + } catch (e) { + LogService.error("ScalarStore", e); + if (!isNaN(Number(e))) { + if (e === 401 || e === 403) { + LogService.info("ScalarStore", "Test user token expired"); + } else { + // Assume offline + return false; + } + } + } + } + + // If we're here, we need to register a new token + + if (existingTokens && existingTokens.length) { + for (const token of existingTokens) { + await token.destroy(); + } + } + + const openId = await MatrixStickerBot.getOpenId(); + const token = await scalarClient.register(openId); + await UserScalarToken.create({ + userId: testUserId, + scalarToken: token.scalar_token, + isDimensionToken: false, + upstreamId: upstream.id, + }); + + return true; + } catch (e) { + LogService.error("ScalarStore", e); + return false; + } + } + private constructor() { } } \ No newline at end of file diff --git a/src/matrix/MatrixLiteClient.ts b/src/matrix/MatrixLiteClient.ts index 1b221fd..6d7d5ca 100644 --- a/src/matrix/MatrixLiteClient.ts +++ b/src/matrix/MatrixLiteClient.ts @@ -2,6 +2,7 @@ import { doClientApiCall } from "./helpers"; import config from "../config"; import * as request from "request"; import { LogService } from "matrix-js-snippets"; +import { OpenId } from "../models/OpenId"; export interface MatrixUrlPreview { // This is really the only parameter we care about @@ -44,6 +45,14 @@ export class MatrixLiteClient { return response['user_id']; } + public async getOpenId(): Promise { + return await doClientApiCall( + "POST", + `/_matrix/client/r0/user/${await this.whoAmI()}/openid/request_token`, + {access_token: this.accessToken}, + ); + } + public async leaveRoom(roomId: string): Promise { return doClientApiCall( "POST", diff --git a/src/matrix/MatrixStickerBot.ts b/src/matrix/MatrixStickerBot.ts index e36be16..09b69ae 100644 --- a/src/matrix/MatrixStickerBot.ts +++ b/src/matrix/MatrixStickerBot.ts @@ -11,6 +11,7 @@ import Sticker from "../db/models/Sticker"; import { MatrixLiteClient } from "./MatrixLiteClient"; import { Cache, CACHE_STICKERS } from "../MemoryCache"; import { LicenseMap } from "../utils/LicenseMap"; +import { OpenId } from "../models/OpenId"; class _MatrixStickerBot { @@ -36,6 +37,11 @@ class _MatrixStickerBot { return this.client.getUserId(); } + public getOpenId(): Promise { + const liteClient = new MatrixLiteClient(config.homeserver.accessToken); + return liteClient.getOpenId(); + } + private async onEvent(roomId, event) { LogService.info("MatrixStickerBot", `Event ${event.type} in ${roomId}`); if (event.type !== "io.t2bot.stickers.metadata" || event.state_key !== "") return; diff --git a/src/scalar/ScalarClient.ts b/src/scalar/ScalarClient.ts index 47ed0b2..8902843 100644 --- a/src/scalar/ScalarClient.ts +++ b/src/scalar/ScalarClient.ts @@ -1,5 +1,5 @@ import { OpenId } from "../models/OpenId"; -import { ScalarRegisterResponse } from "../models/ScalarResponses"; +import { ScalarAccountResponse, ScalarRegisterResponse } from "../models/ScalarResponses"; import * as request from "request"; import { LogService } from "matrix-js-snippets"; import Upstream from "../db/models/Upstream"; @@ -31,4 +31,26 @@ export class ScalarClient { }); }); } + + public getAccount(token: string): Promise { + LogService.info("ScalarClient", "Doing upstream scalar request: " + this.upstream.scalarUrl + "/account"); + return new Promise((resolve, reject) => { + request({ + method: "GET", + url: this.upstream.scalarUrl + "/account", + qs: {v: SCALAR_API_VERSION, scalar_token: token}, + }, (err, res, _body) => { + if (err) { + LogService.error("ScalarClient", "Error getting information for token"); + LogService.error("ScalarClient", err); + reject(err); + } else if (res.statusCode !== 200) { + LogService.error("ScalarClient", "Got status code " + res.statusCode + " while getting information for token"); + reject(res.statusCode); + } else { + resolve(res.body); + } + }); + }); + } } \ No newline at end of file