Skip to content
Snippets Groups Projects
Commit f705ed93 authored by Tulir Asokan's avatar Tulir Asokan :cat2:
Browse files

Add logout endpoint and shared secret for API access

parent ffae9968
No related branches found
No related tags found
No related merge requests found
...@@ -37,6 +37,10 @@ appservice: ...@@ -37,6 +37,10 @@ appservice:
# The base URL where the public-facing endpoints are available. The prefix is not added # The base URL where the public-facing endpoints are available. The prefix is not added
# implicitly. # implicitly.
external: https://example.com/public external: https://example.com/public
# Shared secret for integration managers such as Dimension.
# If set to "generate", a random string will be generated on the next startup.
# If null, integration manager access to the API will not be possible.
shared_secret: generate
# The unique ID of this appservice. # The unique ID of this appservice.
id: facebook id: facebook
......
...@@ -55,7 +55,7 @@ class MessengerBridge(Bridge): ...@@ -55,7 +55,7 @@ class MessengerBridge(Bridge):
self._prepare_website() self._prepare_website()
def _prepare_website(self) -> None: def _prepare_website(self) -> None:
self.public_website = PublicBridgeWebsite() self.public_website = PublicBridgeWebsite(self.config["appservice.public.shared_secret"])
self.az.app.add_subapp(self.config["appservice.public.prefix"], self.public_website.app) self.az.app.add_subapp(self.config["appservice.public.prefix"], self.public_website.app)
def prepare_shutdown(self) -> None: def prepare_shutdown(self) -> None:
......
...@@ -40,6 +40,10 @@ class Config(BaseBridgeConfig): ...@@ -40,6 +40,10 @@ class Config(BaseBridgeConfig):
copy("appservice.public.enabled") copy("appservice.public.enabled")
copy("appservice.public.prefix") copy("appservice.public.prefix")
copy("appservice.public.external") copy("appservice.public.external")
if self["appservice.public.shared_secret"] == "generate":
base["appservice.public.shared_secret"] = self._new_token()
else:
copy("appservice.public.shared_secret")
copy("bridge.username_template") copy("bridge.username_template")
copy("bridge.displayname_template") copy("bridge.displayname_template")
...@@ -56,13 +60,13 @@ class Config(BaseBridgeConfig): ...@@ -56,13 +60,13 @@ class Config(BaseBridgeConfig):
copy_dict("bridge.permissions") copy_dict("bridge.permissions")
def _get_permissions(self, key: str) -> Tuple[bool, bool]: def _get_permissions(self, key: str) -> Tuple[bool, bool, str]:
level = self["bridge.permissions"].get(key, "") level = self["bridge.permissions"].get(key, "")
admin = level == "admin" admin = level == "admin"
user = level == "user" or admin user = level == "user" or admin
return user, admin return user, admin, level
def get_permissions(self, mxid: UserID) -> Tuple[bool, bool]: def get_permissions(self, mxid: UserID) -> Tuple[bool, bool, str]:
permissions = self["bridge.permissions"] or {} permissions = self["bridge.permissions"] or {}
if mxid in permissions: if mxid in permissions:
return self._get_permissions(mxid) return self._get_permissions(mxid)
......
...@@ -48,6 +48,7 @@ class User(Client): ...@@ -48,6 +48,7 @@ class User(Client):
command_status: Optional[Dict[str, Any]] command_status: Optional[Dict[str, Any]]
is_whitelisted: bool is_whitelisted: bool
is_admin: bool is_admin: bool
permission_level: str
_is_logged_in: Optional[bool] _is_logged_in: Optional[bool]
_session_data: Optional[SimpleCookie] _session_data: Optional[SimpleCookie]
_db_instance: Optional[DBUser] _db_instance: Optional[DBUser]
...@@ -62,7 +63,7 @@ class User(Client): ...@@ -62,7 +63,7 @@ class User(Client):
self.by_mxid[mxid] = self self.by_mxid[mxid] = self
self.user_agent = user_agent self.user_agent = user_agent
self.command_status = None self.command_status = None
self.is_whitelisted, self.is_admin = config.get_permissions(mxid) self.is_whitelisted, self.is_admin, self.permission_level = config.get_permissions(mxid)
self._is_logged_in = None self._is_logged_in = None
self._session_data = session self._session_data = session
self._db_instance = db_instance self._db_instance = db_instance
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
# #
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Optional from typing import Optional, Dict
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
import logging import logging
import random import random
...@@ -23,25 +23,32 @@ import json ...@@ -23,25 +23,32 @@ import json
from aiohttp import web from aiohttp import web
import pkg_resources import pkg_resources
import attr
from fbchat import User as FBUser
from mautrix.types import UserID from mautrix.types import UserID
from mautrix.util.signed_token import verify_token from mautrix.util.signed_token import verify_token
from .. import user as u from .. import user as u, puppet as pu
class PublicBridgeWebsite: class PublicBridgeWebsite:
log: logging.Logger = logging.getLogger("ma.web.public") log: logging.Logger = logging.getLogger("mau.web.public")
app: web.Application app: web.Application
secret_key: str secret_key: str
shared_secret: str
def __init__(self) -> None: def __init__(self, shared_secret: str) -> None:
self.app = web.Application() self.app = web.Application()
self.secret_key = "".join(random.choices(string.ascii_lowercase + string.digits, k=64)) self.secret_key = "".join(random.choices(string.ascii_lowercase + string.digits, k=64))
self.app.router.add_static("/", pkg_resources.resource_filename("mautrix_facebook", self.shared_secret = shared_secret
"web/static/")) self.app.router.add_get("/api/whoami", self.status)
self.app.router.add_options("/api/login", self.login_options) self.app.router.add_options("/api/login", self.login_options)
self.app.router.add_post("/api/login", self.login) self.app.router.add_post("/api/login", self.login)
self.app.router.add_post("/api/logout", self.login)
self.app.router.add_static("/", pkg_resources.resource_filename("mautrix_facebook",
"web/static/"))
def verify_token(self, token: str) -> Optional[UserID]: def verify_token(self, token: str) -> Optional[UserID]:
token = verify_token(self.secret_key, token) token = verify_token(self.secret_key, token)
...@@ -49,40 +56,69 @@ class PublicBridgeWebsite: ...@@ -49,40 +56,69 @@ class PublicBridgeWebsite:
return UserID(token.get("mxid")) return UserID(token.get("mxid"))
return None return None
@staticmethod @property
async def login_options(_: web.Request) -> web.Response: def _headers(self) -> Dict[str, str]:
return web.Response(status=200, headers={ return {
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Authorization, Content-Type", "Access-Control-Allow-Headers": "Authorization, Content-Type",
"Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Methods": "POST, OPTIONS",
}) "Content-Type": "application/json",
}
async def login(self, request: web.Request) -> web.Response: async def login_options(self, _: web.Request) -> web.Response:
headers = (await self.login_options(request)).headers return web.Response(status=200, headers=self._headers)
def check_token(self, request: web.Request) -> Optional['u.User']:
try: try:
token = request.headers["Authorization"] token = request.headers["Authorization"]
token = token[len("Bearer "):] token = token[len("Bearer "):]
user_id = self.verify_token(token)
except KeyError: except KeyError:
return web.json_response({"error": "Missing Authorization header"}, status=403, raise web.HTTPBadRequest(body='{"error": "Missing Authorization header"}',
headers=headers) headers=self._headers)
except IndexError: except IndexError:
return web.json_response({"error": "Malformed Authorization header"}, status=401, raise web.HTTPBadRequest(body='{"error": "Malformed Authorization header"}',
headers=headers) headers=self._headers)
if not user_id: if self.shared_secret and token == self.shared_secret:
return web.json_response({"error": "Invalid token"}, status=401, headers=headers) try:
user_id = request.query["user_id"]
except KeyError:
raise web.HTTPBadRequest(body='{"error": "Missing user_id query param"}',
headers=self._headers)
else:
user_id = self.verify_token(token)
if not user_id:
raise web.HTTPForbidden(body='{"error": "Invalid token"}', headers=self._headers)
user = u.User.get_by_mxid(user_id)
return user
async def status(self, request: web.Request) -> web.Response:
print("HI")
user = self.check_token(request)
data = {
"permissions": user.permission_level,
"mxid": user.mxid,
"facebook": None,
}
if await user.is_logged_in():
info: FBUser = (await user.fetch_user_info(user.fbid))[user.fbid]
data["facebook"] = attr.asdict(info)
return web.json_response(data)
async def login(self, request: web.Request) -> web.Response:
user = self.check_token(request)
try: try:
user_agent = request.headers["User-Agent"] user_agent = request.headers["User-Agent"]
except KeyError: except KeyError:
return web.json_response({"error": "Missing User-Agent header"}, status=400, return web.json_response({"error": "Missing User-Agent header"}, status=400,
headers=headers) headers=self._headers)
try: try:
data = await request.json() data = await request.json()
except json.JSONDecodeError: except json.JSONDecodeError:
return web.json_response({"error": "Malformed JSON"}, status=400, headers=headers) return web.json_response({"error": "Malformed JSON"}, status=400,
headers=self._headers)
user = u.User.get_by_mxid(user_id)
cookie = SimpleCookie() cookie = SimpleCookie()
cookie["c_user"] = data["c_user"] cookie["c_user"] = data["c_user"]
cookie["xs"] = data["xs"] cookie["xs"] = data["xs"]
...@@ -91,8 +127,17 @@ class PublicBridgeWebsite: ...@@ -91,8 +127,17 @@ class PublicBridgeWebsite:
ok = await user.set_session(cookie, user_agent) and await user.is_logged_in(True) ok = await user.set_session(cookie, user_agent) and await user.is_logged_in(True)
if not ok: if not ok:
return web.json_response({"error": "Facebook authorization failed"}, status=401, return web.json_response({"error": "Facebook authorization failed"}, status=401,
headers=headers) headers=self._headers)
await user.on_logged_in(data["c_user"]) await user.on_logged_in(data["c_user"])
if user.command_status and user.command_status.get("action") == "Login": if user.command_status and user.command_status.get("action") == "Login":
user.command_status = None user.command_status = None
return web.json_response({}, status=200, headers=headers) return web.json_response({}, status=200, headers=self._headers)
async def logout(self, request: web.Request) -> web.Response:
user = self.check_token(request)
puppet = pu.Puppet.get_by_fbid(user.uid)
await user.logout()
if puppet.is_real_user:
await puppet.switch_mxid(None, None)
return web.json_response({})
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment