diff --git a/mautrix_telegram/web/common/auth_api.py b/mautrix_telegram/web/common/auth_api.py index 3ce557ba..6980f1af 100644 --- a/mautrix_telegram/web/common/auth_api.py +++ b/mautrix_telegram/web/common/auth_api.py @@ -43,29 +43,34 @@ class AuthAPI(abc.ABC): message="Code requested successfully.") except PhoneNumberInvalidError: return self.get_login_response(mxid=user.mxid, state="request", status=400, + errcode="phone_number_invalid", error="Invalid phone number.") + except PhoneNumberBannedError: + return self.get_login_response(mxid=user.mxid, state="request", status=403, + errcode="phone_number_banned", + error="Your phone number is banned from Telegram.") + except PhoneNumberAppSignupForbiddenError: + return self.get_login_response(mxid=user.mxid, state="request", status=403, + errcode="phone_number_app_signup_forbidden", + error="You have disabled 3rd party apps on your account.") except PhoneNumberUnoccupiedError: return self.get_login_response(mxid=user.mxid, state="request", status=404, + errcode="phone_number_unoccupied", error="That phone number has not been registered.") except PhoneNumberFloodError: return self.get_login_response( - mxid=user.mxid, state="request", status=429, + mxid=user.mxid, state="request", status=429, errcode="phone_number_flood", error="Your phone number has been temporarily blocked for flooding. " "The ban is usually applied for around a day.") except FloodWaitError as e: return self.get_login_response( - mxid=user.mxid, state="request", status=429, + mxid=user.mxid, state="request", status=429, errcode="flood_wait", error="Your phone number has been temporarily blocked for flooding. " f"Please wait for {format_duration(e.seconds)} before trying again.") - except PhoneNumberBannedError: - return self.get_login_response(mxid=user.mxid, state="request", status=401, - error="Your phone number is banned from Telegram.") - except PhoneNumberAppSignupForbiddenError: - return self.get_login_response(mxid=user.mxid, state="request", status=401, - error="You have disabled 3rd party apps on your account.") except Exception: self.log.exception("Error requesting phone code") return self.get_login_response(mxid=user.mxid, state="request", status=500, + errcode="exception", error="Internal server error while requesting code.") async def post_login_token(self, user, token): @@ -76,6 +81,14 @@ class AuthAPI(abc.ABC): user.command_status = None return self.get_login_response(mxid=user.mxid, state="logged-in", status=200, username=user_info.username) + except AccessTokenInvalidError: + return self.get_login_response(mxid=user.mxid, state="token", status=401, + errcode="bot_token_invalid", + error="Bot token invalid.") + except AccessTokenExpiredError: + return self.get_login_response(mxid=user.mxid, state="token", status=403, + errcode="bot_token_expired", + error="Bot token expired.") except Exception: self.log.exception("Error sending bot token") return self.get_login_response(mxid=user.mxid, state="token", status=500, @@ -90,10 +103,12 @@ class AuthAPI(abc.ABC): return self.get_login_response(mxid=user.mxid, state="logged-in", status=200, username=user_info.username) except PhoneCodeInvalidError: - return self.get_login_response(mxid=user.mxid, state="code", status=403, + return self.get_login_response(mxid=user.mxid, state="code", status=401, + errcode="phone_code_invalid", error="Incorrect phone code.") except PhoneCodeExpiredError: return self.get_login_response(mxid=user.mxid, state="code", status=403, + errcode="phone_code_expired", error="Phone code expired.") except SessionPasswordNeededError: if not password_in_data: @@ -103,12 +118,13 @@ class AuthAPI(abc.ABC): "action": "Login (password entry)", } return self.get_login_response( - mxid=user.mxid, state="password", status=200, + mxid=user.mxid, state="password", status=202, message="Code accepted, but you have 2-factor authentication is enabled.") return None except Exception: self.log.exception("Error sending phone code") return self.get_login_response(mxid=user.mxid, state="code", status=500, + errcode="exception", error="Internal server error while sending code.") async def post_login_password(self, user, password): @@ -119,10 +135,16 @@ class AuthAPI(abc.ABC): user.command_status = None return self.get_login_response(mxid=user.mxid, state="logged-in", status=200, username=user_info.username) - except (PasswordHashInvalidError, PasswordEmptyError): + except PasswordEmptyError: return self.get_login_response(mxid=user.mxid, state="password", status=400, + errcode="password_empty", + error="Empty password.") + except PasswordHashInvalidError: + return self.get_login_response(mxid=user.mxid, state="password", status=401, + errcode="password_invalid", error="Incorrect password.") except Exception: self.log.exception("Error sending password") return self.get_login_response(mxid=user.mxid, state="password", status=500, + errcode="exception", error="Internal server error while sending password.") diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index db89c506..05c73816 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -17,11 +17,75 @@ from aiohttp import web import logging +from ..common import AuthAPI -class ProvisioningAPI: + +class ProvisioningAPI(AuthAPI): log = logging.getLogger("mau.provisioning") def __init__(self, loop): - self.loop = loop + super(AuthAPI, self).__init__(loop) self.app = web.Application(loop=loop) + + login_prefix = "/login/{mxid:@[^:]*:.+}" + self.app.router.add_route("POST", f"{login_prefix}/bot_token", self.send_bot_token) + self.app.router.add_route("POST", f"{login_prefix}/request_code", self.request_code) + self.app.router.add_route("POST", f"{login_prefix}/send_code", self.send_code) + self.app.router.add_route("POST", f"{login_prefix}/send_password", self.send_password) + + def get_login_response(self, status=200, state="", username="", mxid="", message="", error="", + errcode=""): + if username: + resp = { + "state": "logged-in", + "username": username, + } + elif message: + resp = { + "message": message + } + else: + resp = { + "error": error, + "errcode": errcode, + } + return web.json_response(resp, status=status) + + async def get_user(self, request: web.Request): + mxid = request.match_info["mxid"] + user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True) + if not user.puppet_whitelisted: + return user, self.get_login_response(mxid=user.mxid, error="You are not whitelisted.", + errcode="mxid_not_whitelisted", status=403) + elif await user.is_logged_in(): + return user, self.get_login_response(mxid=user.mxid, username=user.username, status=409) + return user, None + + async def send_bot_token(self, request: web.Request): + user, err = await self.get_user(request) + if err: + return err + data = await request.json() + return await self.post_login_token(user, data.get("token", "")) + + async def request_code(self, request: web.Request): + user, err = await self.get_user(request) + if err: + return err + data = await request.json() + return await self.post_login_phone(user, data.get("phone", "")) + + async def send_code(self, request: web.Request): + user, err = await self.get_user(request) + if err: + return err + data = await request.json() + return await self.post_login_code(user, data.get("code", 0), password_in_data=False) + + async def send_password(self, request: web.Request): + user, err = await self.get_user(request) + if err: + return err + data = await request.json() + return await self.post_login_password(user, data.get("password", "")) diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml index 7b0dc01f..e8fe08f3 100644 --- a/mautrix_telegram/web/provisioning/spec.yaml +++ b/mautrix_telegram/web/provisioning/spec.yaml @@ -1,70 +1,395 @@ +swagger: "2.0" + +info: + title: mautrix-telegram provisioning + version: 0.3.0 + description: The provisioning API for mautrix-telegram. + contact: + name: Tulir Asokan + email: tulir@maunium.net + url: https://maunium.net + license: + name: AGPLv3 + url: https://github.com/tulir/mautrix-telegram/blob/master/LICENSE + +externalDocs: + description: Provisioning API wiki page on GitHub. + url: https://github.com/tulir/mautrix-telegram/wiki/Provisioning-API + +basePath: /_matrix/provision + +schemes: [https] +consumes: [application/json] +produces: [application/json] + tags: - - - name: login - description: 'Authentication endpoints.' +- name: Authentication + +paths: + /login/{mxid}/bot_token: + post: + operationId: post_bot_token + summary: Log in with a bot token + tags: [Authentication] + responses: + 200: + description: Login successful + schema: + $ref: "#/definitions/AuthSuccess" + 400: + $ref: "#/responses/MissingMXIDError" + 401: + description: Invalid or expired bot token + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + example: bot_token_ + enum: + - bot_token_invalid + - bot_token_expired + error: + $ref: "#/definitions/HumanReadableError" + 403: + $ref: "#/responses/NotWhitelistedError" + 409: + $ref: "#/responses/AlreadyLoggedInError" + 500: + $ref: "#/responses/UnknownError" + parameters: + - name: mxid + in: path + description: The Matrix ID of the user who to log in as + required: true + type: string + - name: body + in: body + required: true + schema: + type: object + properties: + token: + type: string + description: The access token of the bot to log in as + example: "297900271:IXjeGEcAN61zHnjPgkWnYWyvVp9K4ulHBEv" + /login/{mxid}/request_code: + post: + operationId: post_login_phone + summary: Request a phone code from Telegram + tags: [Authentication] + responses: + 200: + description: Code requested successfully + schema: + $ref: "#/definitions/AuthSuccess" + 400: + description: Invalid phone number or missing Matrix ID + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + example: machine_readable_error + enum: + - phone_number_invalid + - mxid_empty + error: + $ref: "#/definitions/HumanReadableError" + 403: + description: Matrix ID is not whitelisted or phone number is banned or has forbidden 3rd party apps + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + example: machine_readable_error + enum: + - mxid_not_whitelisted + - phone_number_banned + - phone_number_app_signup_forbidden + error: + $ref: "#/definitions/HumanReadableError" + 404: + description: Unregistered phone number + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - phone_number_unoccupied + error: + $ref: "#/definitions/HumanReadableError" + 409: + $ref: "#/responses/AlreadyLoggedInError" + 429: + description: Phone number has been temporarily blocked for flooding + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - flood_wait + - phone_number_flood + error: + $ref: "#/definitions/HumanReadableError" + 500: + $ref: "#/responses/UnknownError" + parameters: + - name: mxid + in: path + description: The Matrix ID of the user who to log in as + required: true + type: string + - name: body + in: body + required: true + schema: + type: object + properties: + phone: + type: string + description: The phone number to log in as. + example: "+123456789" + /login/{mxid}/send_code: + post: + operationId: post_login_code + summary: Send the login code + tags: [Authentication] + responses: + 200: + description: Login successful + schema: + $ref: "#/definitions/AuthSuccess" + 202: + description: Correct code, but two-factor authentication is enabled + schema: + $ref: "#/definitions/AuthSuccess" + 400: + $ref: "#/responses/MissingMXIDError" + 401: + description: Invalid phone code + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - phone_code_invalid + error: + $ref: "#/definitions/HumanReadableError" + 403: + description: Matrix ID not whitelisted or phone code expired + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + example: machine_readable_error + enum: + - mxid_not_whitelisted + - phone_code_expired + error: + $ref: "#/definitions/HumanReadableError" + 409: + $ref: "#/responses/AlreadyLoggedInError" + 500: + $ref: "#/responses/UnknownError" + parameters: + - name: mxid + in: path + description: The Matrix ID of the user who to log in as + required: true + type: string + - name: body + in: body + required: true + schema: + type: object + properties: + code: + type: integer + description: The phone code from Telegram. + format: int32 + example: 123456 + /login/{mxid}/send_password: + post: + operationId: post_login_password + summary: Send the two-factor auth password + tags: [Authentication] + responses: + 200: + description: Login successful + schema: + $ref: "#/definitions/AuthSuccess" + 400: + description: Missing password or Matrix ID + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + example: _empty + enum: + - password_empty + - mxid_empty + error: + $ref: "#/definitions/HumanReadableError" + 401: + description: Incorrect password + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - password_invalid + error: + $ref: "#/definitions/HumanReadableError" + 403: + $ref: "#/responses/NotWhitelistedError" + 409: + $ref: "#/responses/AlreadyLoggedInError" + 500: + $ref: "#/responses/UnknownError" + parameters: + - name: mxid + in: path + description: The Matrix ID of the user who to log in as + required: true + type: string + - name: body + in: body + required: true + schema: + type: object + properties: + password: + type: string + description: The two-factor auth password + format: password + example: hunter2 + +responses: + NotWhitelistedError: + description: Matrix ID not whitelisted for puppeting + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - mxid_not_whitelisted + error: + $ref: "#/definitions/HumanReadableError" + AlreadyLoggedInError: + description: The Matrix user is already logged in + schema: + type: object + properties: + state: + type: string + enum: + - logged-in + username: + type: string + description: The Telegram username the user is logged in as. + MissingMXIDError: + description: Missing Matrix ID + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - mxid_empty + error: + $ref: "#/definitions/HumanReadableError" + UnknownError: + description: Unknown error + schema: + type: object + title: UnknownError + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - exception + error: + type: string + title: Error + description: A human-readable description of the error + example: Internal server error while . + enum: + - Internal server error while requesting code. + - Internal server error while sending code. + - Internal server error while sending password. + - Internal server error while sending token. + definitions: - Error: - x-oad-type: object - type: object - title: Error - properties: - errcode: - x-oad-type: string - type: string - title: 'Error code' - description: 'A machine-readable error code' - error: - x-oad-type: string - type: string - title: Error - description: 'A human-readable description of the error' - status: - x-oad-type: integer - type: integer - title: Status - description: 'The HTTP status code' - format: int32 + HumanReadableError: + type: string + description: A human-readable description of the error + example: A human-readable description of the error AuthSuccess: - x-oad-type: object type: object properties: state: - x-oad-type: string type: string + description: The state/next step after the successful operation. enum: - - code - - request - - password - - token - - logged-in + - code + - request + - password + - token + - logged-in + username: + type: string + description: The Telegram username the user is logged in as. Only applicable if state=logged-in + + security: - - - Bearer: [] + - Bearer: [] securityDefinitions: Bearer: - description: 'Required authentication for all endpoints' + description: Required authentication for all endpoints name: Authorization in: header type: apiKey -info: - title: 'mautrix-telegram provisioning' - version: 0.3.0 - description: 'The provisioning API for mautrix-telegram.' - contact: - name: 'Tulir Asokan' - email: tulir@maunium.net - url: 'https://maunium.net' - license: - name: AGPLv3 - url: 'https://github.com/tulir/mautrix-telegram/blob/master/LICENSE' -externalDocs: - description: 'Provisioning API wiki page on GitHub.' - url: 'https://github.com/tulir/mautrix-telegram/wiki/Provisioning-API' -basePath: /_matrix/provisioning -schemes: - - https -consumes: - - application/json -produces: - - application/json -swagger: '2.0' diff --git a/mautrix_telegram/web/public/__init__.py b/mautrix_telegram/web/public/__init__.py index 89a76fd7..7a83d731 100644 --- a/mautrix_telegram/web/public/__init__.py +++ b/mautrix_telegram/web/public/__init__.py @@ -16,7 +16,6 @@ # along with this program. If not, see . from aiohttp import web from mako.template import Template -import asyncio import pkg_resources import logging