"""Disposable socket coverage for Galaxy pairing and tunnel auth boundaries.""" import base64 import hashlib import http.client import json from pathlib import Path import tempfile import threading import unittest from unittest import mock from openpilot.starpilot.galaxy.access import GalaxyAccessOwner from openpilot.starpilot.galaxy.remote import RemotePairing, RemoteTokens, gateway_cookie_valid, make_gateway_auth_server from openpilot.starpilot.galaxy.server import make_remote_server, make_server class RemotePairingTest(unittest.TestCase): def setUp(self): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) claim_path = mock.patch('openpilot.starpilot.galaxy_bluetooth.bridge.claim.CLAIM_PATH', Path(temporary.name) / 'claim.json') claim_path.start() self.addCleanup(claim_path.stop) root = Path(temporary.name) self.owner = GalaxyAccessOwner(root / 'credentials') self.pairing = RemotePairing(root / 'pairing') class Monitor: def sample(self): return {'schemaVersion': 1, 'mode': 'disposable'} self.local = make_server(port=0, owner=self.owner, remote_pairing=self.pairing, parked=lambda: True, monitor=Monitor()) self.remote = make_remote_server(self.local, port=0) self.workers = [] for server in (self.local, self.remote): worker = threading.Thread(target=server.serve_forever, kwargs={'poll_interval': .01}, daemon=True) worker.start() self.workers.append(worker) self.addCleanup(self.close_servers) def close_servers(self): for server, worker in zip((self.remote, self.local), reversed(self.workers), strict=True): server.shutdown() worker.join(timeout=2) server.server_close() def request(self, server, path, *, method='GET', payload=None, host=None, origin=None, cookie=None): connection = http.client.HTTPConnection('127.0.0.1', server.server_port, timeout=2) headers = {'Host': host or f'127.0.0.1:{server.server_port}'} if payload is not None: headers['Content-Type'] = 'application/json' if origin is not None: headers['Origin'] = origin if cookie is not None: headers['Cookie'] = cookie connection.request(method, path, body=json.dumps(payload) if payload is not None else None, headers=headers) response = connection.getresponse() result = response.status, response.read(), dict(response.getheaders()) connection.close() return result def test_app_key_export_requires_direct_session_or_matching_gateway_cookie(self): for path in ('/api/galaxy/session', '/api/galaxy/session/qr.svg'): self.assertEqual(self.request(self.local, path)[0], 401) cookie = self.request(self.local, '/api/auth/session')[2]['Set-Cookie'].split(';', 1)[0] code, body, headers = self.request(self.local, '/api/galaxy/session', cookie=cookie) self.assertEqual((code, json.loads(body)), (200, {'paired': False})) self.assertEqual(headers['Cache-Control'], 'no-store') slug = self.pairing.pair(hashlib.sha256(b'password123').hexdigest()) record = self.pairing.read() assert record is not None token = f"{slug}%3A{record['session']}" for path in ('/api/galaxy/session', '/api/galaxy/session/qr.svg'): self.assertEqual(self.request(self.local, path, cookie=cookie)[0], 200) with mock.patch('openpilot.starpilot.galaxy.server.direct_local_connection', return_value=False): self.assertEqual(self.request(self.local, path, cookie=cookie)[0], 403) self.assertEqual(self.request(self.remote, path, host='galaxy.firestar.link', cookie=cookie)[0], 403) self.assertEqual(self.request(self.remote, path, host='galaxy.firestar.link', cookie='galaxy_session=wrong')[0], 403) code, body, headers = self.request(self.remote, f'/{slug}' + path, host='galaxy.firestar.link', cookie='galaxy_session=' + token) self.assertEqual(code, 200) self.assertEqual(headers['Cache-Control'], 'no-store') if not path.endswith('.svg'): self.assertEqual(json.loads(body)['sessionToken'], token) else: self.assertIn(b'