diff --git a/am_i_up/Api.py b/am_i_up/Api.py index 3eab44a..be107b3 100644 --- a/am_i_up/Api.py +++ b/am_i_up/Api.py @@ -11,17 +11,20 @@ from aiohttp import web import asyncio from ament_index_python import get_package_share_directory +from am_i_up.Authenticator import Authenticator import rclpy class Api: def __init__(self, facts): self._facts = facts + self._authenticator = Authenticator(facts.get_password(), facts.get_open_endpoints()) + async def run(self): ui_share_directory = get_package_share_directory(self._facts.get_ui_pkg()) ui_static_directory = ui_share_directory + "/dist/assets" - app = web.Application() + app = web.Application(middlewares=[self._authenticator.build_middleware()]) app.add_routes([ web.get('/api/ping', self.ping), web.get('/api/uptime', self.uptime), @@ -30,6 +33,7 @@ class Api: web.get('/api/status', self.status), web.get("/api/position", self.position), web.get("/api/diagnostics", self.diagnostics), + web.post("/api/login", self.login), web.static("/assets", ui_static_directory), # we're not actually using key anywhere, but doing this allows react router # to work correctly. @@ -49,6 +53,23 @@ class Api: await asyncio.sleep(3600) await runner.cleanup() + + async def login(self, request): + request_dict = await request.json() + login_successful = self._authenticator.confirm_creds(request_dict["password"]) + if not login_successful: + return web.Response(status=400) + + redirect_location = f"{request.url.scheme}://{request.url.host_port_subcomponent}/" + redirect_header = {"Location": redirect_location} + response = web.Response(status=302, headers=redirect_header) + response.set_cookie(self._authenticator.cookie_name(), + self._authenticator.make_cookie_for_user(), + secure=False, + httponly=False, + samesite="Strict") + return response + async def index(self, request): ui_share_directory = get_package_share_directory(self._facts.get_ui_pkg()) ui_index_path = ui_share_directory + "/dist/index.html" diff --git a/am_i_up/Authenticator.py b/am_i_up/Authenticator.py new file mode 100644 index 0000000..dc53565 --- /dev/null +++ b/am_i_up/Authenticator.py @@ -0,0 +1,47 @@ +from aiohttp import web +import secrets +import re + +# TODO: Note here about how this is not supposed to be put on the +# internet and if you wanted to do that it needs to be done +# differently. + +class Authenticator(): + def __init__(self, password, open_endpoints): + self._password = password + self._open_endpoints = open_endpoints + self._valid_cookies = [] + + def confirm_creds(self, password): + return secrets.compare_digest(password, self._password) + + def make_cookie_for_user(self): + cookie = secrets.token_urlsafe(32) + # TODO: Normally these would expire, but we're assuming + # this app gets closed like once a day so. + self._valid_cookies.append(cookie) + return cookie + + def confirm_user_from_cookie(self, cookie): + return cookie in self._valid_cookies + + def cookie_name(self): + return "auth_cookie" + + def build_middleware(self): + @web.middleware + async def auth_middleware(request, handler): + for open_endpoint in self._open_endpoints: + if re.fullmatch(open_endpoint, request.path): + return await handler(request) + if self.cookie_name() in request.cookies: + cookie = request.cookies[self.cookie_name()] + if self.confirm_user_from_cookie(cookie): + return await handler(request) + redirect_location = f"{request.url.scheme}://{request.url.host_port_subcomponent}/login" + redirect_header = {"Location": redirect_location} + return web.Response(status=302, headers=redirect_header) + + return auth_middleware + + diff --git a/am_i_up/Facts.py b/am_i_up/Facts.py index afa51f1..b43488d 100644 --- a/am_i_up/Facts.py +++ b/am_i_up/Facts.py @@ -37,6 +37,8 @@ class Facts: self._listen_port = self._node.declare_parameter("listen_port", "8888").value self._ui_pkg = self._node.declare_parameter("ui_pkg", "am_i_up_ui").value + self._password = self._node.declare_parameter("password", "password").value + self._status_string = None self._lat_long = None self._diag_msg = None @@ -114,6 +116,13 @@ class Facts: print("Can't find build info.\n{}".format(e)) return project_state_content + def get_password(self): + return self._password + + def get_open_endpoints(self): + # Hardcoding for now, can make a parameter later. + return ["/login", "/api/login", "/assets/(.*)"] + def _status_callback(self, msg): self._status_string = msg.data diff --git a/am_i_up/client.py b/am_i_up/client.py index dad55da..abb43ac 100644 --- a/am_i_up/client.py +++ b/am_i_up/client.py @@ -19,7 +19,7 @@ def main(args=None): parser = argparse.ArgumentParser() parser.add_argument("--action") parser.add_argument("--options", nargs="*") - parser.add_argument("--host", default="http://localhost:8888") + parser.add_argument("--host", default="http://localhost:8000") args = parser.parse_args() @@ -40,6 +40,12 @@ class Client(): def __init__(self, host): self._host = host + async def login(self, session): + request = { "password": "password" } + async with session.post(f'{self._host}/api/login', json=request) as resp: + if not resp.ok: + print("Login failed.") + async def call_ping(self, options): if len(options) != 1 or options[0] == "": raise RunTimeError("Ping option is an address as a string.") @@ -49,6 +55,7 @@ class Client(): print("Calling ping with request: {}", json.dumps(request)) async with aiohttp.ClientSession() as session: + await self.login(session) async with session.get(f'{self._host}/api/ping', json=request) as resp: print(await resp.text()) @@ -56,6 +63,7 @@ class Client(): print("Calling uptime.") async with aiohttp.ClientSession() as session: + await self.login(session) async with session.get(f'{self._host}/api/uptime') as resp: print(await resp.text()) @@ -63,6 +71,7 @@ class Client(): print("Calling build_info.") async with aiohttp.ClientSession() as session: + await self.login(session) async with session.get(f'{self._host}/api/build_info') as resp: print(await resp.text()) @@ -70,6 +79,7 @@ class Client(): print("Calling env.") async with aiohttp.ClientSession() as session: + await self.login(session) async with session.get(f'{self._host}/api/env') as resp: print(await resp.text()) @@ -77,5 +87,6 @@ class Client(): print("Calling diagnostics.") async with aiohttp.ClientSession() as session: + await self.login(session) async with session.get(f'{self._host}/api/diagnostics') as resp: print(await resp.text())