Authentication may or may not now work

This commit is contained in:
Tulir Asokan
2017-11-13 01:19:12 +02:00
parent 9db90d2d5c
commit daeaca8c4f
13 changed files with 1100 additions and 121 deletions
+151
View File
@@ -0,0 +1,151 @@
// mautrix-telegram - A Matrix-Telegram puppeting bridge
// Copyright (C) 2017 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
const {Bridge} = require("matrix-appservice-bridge")
const commands = require("./commands")
const MatrixUser = require("./matrix-user")
class MautrixTelegram {
constructor(config) {
this.config = config
this.matrixUsersByID = new Map()
this.telegramUsersByID = new Map()
const self = this
this.bridge = new Bridge({
homeserverUrl: config.homeserver.address,
domain: config.homeserver.domain,
registration: config.appservice.registration,
controller: {
onUserQuery(user) {
return {}
},
onEvent(request, context) {
self.handleMatrixEvent(request.getData())
},
},
})
}
run() {
console.log("Appservice listening on port %s", this.config.appservice.port)
this.bridge.run(this.config.appservice.port, {})
//this.botIntent.setDisplayName(this.config.bridge.bot_displayname)
}
get bot() {
return this.bridge.getBot()
}
get botIntent() {
return this.bridge.getIntent()
}
getMatrixUser(id) {
let user = this.matrixUsersByID.get(id)
if (user) {
console.log(id, "found in cache")
return Promise.resolve(user)
}
return this.bridge.getUserStore().select({
type: "matrix",
id,
}).then(entries => {
this.matrixUsersByID.get(id)
if (user) {
console.log(id, "found in cache (after race)")
return Promise.resolve(user)
}
if (entries.length) {
user = MatrixUser.fromEntry(this, entries[0])
console.log(id, "loaded from database")
} else {
user = new MatrixUser(this, id)
console.log(id, "created")
}
this.matrixUsersByID.set(id, user)
return user
})
}
putUser(user) {
const entry = user.toEntry()
return this.bridge.getUserStore().upsert({
type: entry.type,
id: entry.id,
}, entry)
}
handleMatrixEvent(evt) {
const asBotID = this.bridge.getBot().getUserId()
if (evt.type === "m.room.member" && evt.state_key === asBotID) {
if (evt.content.membership === "invite") {
// Accept all invites
this.botIntent.join(evt.room_id)
.catch(err => {
console.warn(`Failed to join room ${evt.room_id}:`, err)
if (e instanceof Error) {
console.warn(e.stack)
}
})
}
return
}
if (evt.sender === asBotID || evt.type !== "m.room.message" || !evt.content) {
// Ignore own messages and non-message events.
return;
}
const cmdprefix = this.config.bridge.command_prefix
if (evt.content.body.startsWith(cmdprefix + " ")) {
this.getMatrixUser(evt.sender).then(user => {
if (!user.whitelisted) {
this.botIntent.sendText(evt.room_id, "You are not authorized to use this bridge.")
return
}
const prefixLength = cmdprefix.length + 1
const args = evt.content.body.substr(prefixLength).split(" ")
const command = args.shift()
commands.run(user, command, args, reply =>
this.botIntent.sendText(
evt.room_id,
reply.replace("$cmdprefix", cmdprefix)))
})
return
}
}
checkWhitelist(userID) {
if (!this.config.bridge.whitelist || this.config.bridge.whitelist.length === 0) {
return true
}
userID = userID.toLowerCase()
const userIDCapture = /\@.+\:(.+)/.exec(userID)
const homeserver = userIDCapture && userIDCapture.length > 1 ? userIDCapture[1] : undefined
for (let whitelisted of this.config.bridge.whitelist) {
whitelisted = whitelisted.toLowerCase()
if (whitelisted === userID || (homeserver && whitelisted === homeserver)) {
return true
}
}
return false
}
}
module.exports = MautrixTelegram
+126
View File
@@ -0,0 +1,126 @@
// mautrix-telegram - A Matrix-Telegram puppeting bridge
// Copyright (C) 2017 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
const makePasswordHash = require("telegram-mtproto").plugins.makePasswordHash
class Command {
constructor(description, usage, func) {
this.description = description
this.usage = usage
this.func = func
}
run(app, roomID, args) {
this.func(args, message =>
app.botIntent.sendText(roomID, message))
}
}
const commands = {}
function run(sender, command, args, reply) {
if (sender.commandStatus) {
if (command === "cancel") {
sender.commandStatus = undefined
reply(`${sender.commandStatus.action} cancelled.`)
return
}
args.unshift(command)
sender.commandStatus.next(sender, args, reply)
return
}
command = this.commands[command]
if (!command) {
reply("Unknown command. Try \"$cmdprefix help\" for help.")
}
command(sender, args, reply)
}
commands.cancel = () => "Nothing to cancel."
const enterPassword = (sender, args, reply) => {
if (args.length === 0) {
reply("Usage: $cmdprefix <password>")
return
}
const hash = makePasswordHash(sender.commandStatus.salt, args[0])
sender.checkPassword(hash)
.then(() => {
// TODO show who the user logged in as
reply(`Logged in successfully.`)
sender.commandStatus = undefined
}, err => {
reply(`Login failed: ${err}`)
console.log(err)
})
}
const enterCode = (sender, args, reply) => {
if (args.length === 0) {
reply("Usage: $cmdprefix <authentication code>")
return
}
sender.signInToTelegram(args[0])
.then(data => {
if (data.status === "ok") {
// TODO show who the user logged in as
reply(`Logged in successfully.`)
sender.commandStatus = undefined
} else if (data.status === "need-password") {
reply(`You have two-factor authentication enabled. Password hint: ${data.hint} \nEnter your password using "$cmdprefix <password>"`)
sender.commandStatus = {
action: "Two-factor authentication",
next: enterPassword,
salt: data.salt,
}
} else {
reply(`Unexpected sign in response, status=${data.status}`)
}
}, err => {
reply(`Login failed: ${err}`)
console.log(err)
})
}
commands.login = (sender, args, reply) => {
if (args.length === 0) {
reply("Usage: $cmdprefix login <phone number>")
return
}
sender.sendTelegramCode(args[0])
.then(data => {
reply(`Login code sent to ${args[0]}. \nEnter the code using "$cmdprefix <code>"`)
sender.commandStatus = {
action: "Phone code authentication",
next: enterCode,
}
console.log(data)
}, err => {
reply(`Failed to send code: ${err}`)
console.log(err)
})
}
commands.help = (sender, args, reply) => {
reply("Help not yet implemented 3:")
}
module.exports = {
commands,
run,
}
Executable
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env node
// mautrix-telegram - A Matrix-Telegram puppeting bridge
// Copyright (C) 2017 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
const {AppServiceRegistration} = require("matrix-appservice-bridge")
const commander = require("commander")
const YAML = require("yamljs")
const fs = require("fs")
const MautrixTelegram = require("./app")
const pkg = require("../package.json")
commander
.version(pkg.version)
.option("-c, --config <path>", "the file to load the config from. defaults to ./config.yaml")
.option("-g, --generate-registration", "generate a registration based on the config")
.option("-r, --registration <path>", "the file to save the registration to. defaults to ./registration.yaml")
.parse(process.argv)
commander.registration = commander.registration || "./registration.yaml"
commander.config = commander.config || "./config.yaml"
const config = YAML.load(commander.config)
if (commander.generateRegistration) {
const registration = {
id: config.appservice.id,
hs_token: AppServiceRegistration.generateToken(),
as_token: AppServiceRegistration.generateToken(),
namespaces: {
users: [{
exclusive: true,
regex: `@${config.bridge.username_template.replace("${ID}", ".+")}:${config.homeserver.domain}`
}],
aliases: [],
rooms: [],
},
url: `${config.appservice.protocol}://${config.appservice.hostname}:${config.appservice.port}`,
sender_localpart: config.bridge.bot_username,
rate_limited: false,
}
fs.writeFileSync(commander.registration, YAML.stringify(registration, 10))
config.appservice.registration = commander.registration
fs.writeFileSync(commander.config, YAML.stringify(config, 10))
return
}
const app = new MautrixTelegram(config)
app.run()
+104
View File
@@ -0,0 +1,104 @@
// mautrix-telegram - A Matrix-Telegram puppeting bridge
// Copyright (C) 2017 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
const TelegramPuppet = require("./telegram-puppet")
class MatrixUser {
constructor(app, userID) {
this.app = app
this.userID = userID
this.whitelisted = app.checkWhitelist(userID)
this.phoneNumber = undefined
this.phoneCodeHash = undefined
this.commandStatus = undefined
this._telegramPuppet = undefined
}
static fromEntry(app, entry) {
if (entry.type !== "matrix") {
throw new Error("MatrixUser can only be created from entry type \"matrix\"")
}
const user = new MatrixUser(app, entry.id)
user.phoneNumber = entry.data.phone_number
user.phoneCodeHash = entry.data.phone_code_hash
if (entry.data.puppet) {
user.puppetData = entry.data.puppet
user.telegramPuppet
}
return user
}
toEntry() {
if (this.puppet) {
this.puppetData = this.puppet.toSubentry()
}
return {
type: "matrix",
id: this.userID,
data: {
phone_number: this.phoneNumber,
phone_code_hash: this.phoneCodeHash,
puppet: this.puppetData,
},
}
}
get telegramPuppet() {
if (!this._telegramPuppet) {
this._telegramPuppet = TelegramPuppet.fromSubentry(this.app, this, this.puppetData || {})
}
return this._telegramPuppet
}
parseTelegramError(err) {
const message = err.toPrintable ? err.toPrintable() : err.toString()
if (err instanceof Error) {
throw err
}
throw new Error(message)
}
sendTelegramCode(phoneNumber) {
// TODO handle existing login?
return this.telegramPuppet.sendCode(phoneNumber)
.then(result => {
this.phoneNumber = phoneNumber
this.phoneCodeHash = result.phone_code_hash
this.app.putUser(this)
return result
}, err => this.parseTelegramError(err))
}
signInToTelegram(phoneCode) {
if (!this.phoneNumber) throw new Error("Phone number not set")
if (!this.phoneCodeHash) throw new Error("Phone code not sent")
return this.telegramPuppet.signIn(this.phoneNumber, this.phoneCodeHash, phoneCode)
.then(result => {
this.phoneCodeHash = undefined
return this.app.putUser(this).then(() => result)
})
}
checkPassword(password_hash) {
return this.telegramPuppet.checkPassword(password_hash)
.then(() => this.app.putUser(this))
}
}
module.exports = MatrixUser
+115
View File
@@ -0,0 +1,115 @@
// mautrix-telegram - A Matrix-Telegram puppeting bridge
// Copyright (C) 2017 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
const pkg = require("../package.json")
const os = require("os")
const telegram = require("telegram-mtproto")
class TelegramPuppet {
constructor(opts) {
this._client = undefined
this.userID = opts.userID
this.matrixUser = opts.matrixUser
this.data = opts.data
this.app = opts.app
this.serverConfig = Object.assign({}, opts.server_config)
this.api_hash = opts.api_hash
this.api_id = opts.api_id
this.apiConfig = Object.assign({}, {
app_version: pkg.version,
lang_code: "en",
api_id: opts.api_id,
}, opts.api_config)
}
static fromSubentry(app, matrixUser, data) {
const userID = data.user_id
delete data.user_id
return new TelegramPuppet(Object.assign({
userID,
matrixUser,
data,
app,
}, app.config.telegram))
}
toSubentry() {
return Object.assign({
user_id: this.userID
}, this.data)
}
get datacenter() {
return { dcID: 1 }
}
get client() {
if (!this._client) {
this._client = telegram.MTProto({
api: this.apiConfig,
server: this.serverConfig,
})
}
return this._client
}
sendCode(phone_number) {
return this.client("auth.sendCode", {
phone_number,
current_number: true,
api_id: this.api_id,
api_hash: this.api_hash,
})
}
signIn(phone_number, phone_code_hash, phone_code) {
return this.client("auth.signIn", {
phone_number, phone_code, phone_code_hash
})
.then(
result => this.signInComplete(result),
err => {
if (err.type !== "SESSION_PASSWORD_NEEDED") {
throw err
}
this.client("account.getPassword", {}).then(data => {
return {
status: "need-password",
hint: data.hint,
salt: data.current_salt
}
})
})
}
checkPassword(password_hash) {
return this.client("auth.checkPassword", {password_hash})
.then((result) => this.signInComplete(result))
}
signInComplete(data) {
this.userID = data.user.id
return {
status: "ok"
}
}
}
module.exports = TelegramPuppet