From bfd8faafef028680173612ede3f4ff14db465cbc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 6 Sep 2015 18:43:24 +0200 Subject: [PATCH] Add a protocol version to IPC. Fixes #909. --- qutebrowser/misc/ipc.py | 50 +++++++++++++++++++++++++------------ tests/unit/misc/test_ipc.py | 40 ++++++++++++++++++++++------- 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index 6f60f7fbf..c7a1c5108 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -36,6 +36,7 @@ from qutebrowser.utils import log, usertypes, error, objreg CONNECT_TIMEOUT = 100 WRITE_TIMEOUT = 1000 READ_TIMEOUT = 5000 +PROTOCOL_VERSION = 1 def _get_socketname(basedir, user=None): @@ -228,6 +229,13 @@ class IPCServer(QObject): # Maybe another connection is waiting. self.handle_connection() + def _handle_invalid_data(self): + """Handle invalid data we got from a QLocalSocket.""" + log.ipc.error("Ignoring invalid IPC data.") + self.got_invalid_data.emit() + self._socket.error.connect(self.on_error) + self._socket.disconnectFromServer() + @pyqtSlot() def on_ready_read(self): """Read json data from the client.""" @@ -241,35 +249,44 @@ class IPCServer(QObject): data = bytes(self._socket.readLine()) self.got_raw.emit(data) log.ipc.debug("Read from socket: {}".format(data)) + try: decoded = data.decode('utf-8') except UnicodeDecodeError: - log.ipc.error("Ignoring invalid IPC data.") - log.ipc.debug("invalid data: {}".format( + log.ipc.error("invalid utf-8: {}".format( binascii.hexlify(data))) - self.got_invalid_data.emit() - self._socket.error.connect(self.on_error) - self._socket.disconnectFromServer() + self._handle_invalid_data() return + log.ipc.debug("Processing: {}".format(decoded)) try: json_data = json.loads(decoded) except ValueError: - log.ipc.error("Ignoring invalid IPC data.") - log.ipc.debug("invalid json: {}".format(decoded.strip())) - self.got_invalid_data.emit() - self._socket.error.connect(self.on_error) - self._socket.disconnectFromServer() + log.ipc.error("invalid json: {}".format(decoded.strip())) + self._handle_invalid_data() return + try: args = json_data['args'] except KeyError: - log.ipc.error("Ignoring invalid IPC data.") - log.ipc.debug("no args: {}".format(decoded.strip())) - self.got_invalid_data.emit() - self._socket.error.connect(self.on_error) - self._socket.disconnectFromServer() + log.ipc.error("no args: {}".format(decoded.strip())) + self._handle_invalid_data() return + + try: + protocol_version = int(json_data['protocol_version']) + except (KeyError, ValueError): + log.ipc.error("invalid version: {}".format(decoded.strip())) + self._handle_invalid_data() + return + + if protocol_version != PROTOCOL_VERSION: + log.ipc.error("incompatible version: expected {}, " + "got {}".format( + PROTOCOL_VERSION, protocol_version)) + self._handle_invalid_data() + return + cwd = json_data.get('cwd', None) self.got_args.emit(args, cwd) @@ -310,7 +327,8 @@ def send_to_running_instance(socketname, command, *, socket=None): connected = socket.waitForConnected(100) if connected: log.ipc.info("Opening in existing instance") - json_data = {'args': command, 'version': qutebrowser.__version__} + json_data = {'args': command, 'version': qutebrowser.__version__, + 'protocol_version': PROTOCOL_VERSION} try: cwd = os.getcwd() except OSError: diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index be18d73df..e1c79fc03 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -296,7 +296,9 @@ class TestHandleConnection: assert msg in all_msgs def test_read_line_immediately(self, qtbot, ipc_server, caplog): - socket = FakeSocket(data=b'{"args": ["foo"]}\n') + data = '{{"args": ["foo"], "protocol_version": {}}}\n'.format( + ipc.PROTOCOL_VERSION) + socket = FakeSocket(data=data.encode('utf-8')) ipc_server._server = FakeServer(socket) @@ -333,28 +335,47 @@ def test_partial_line(connected_socket): connected_socket.write(b'foo') -@pytest.mark.parametrize('data', [ - b'\x80\n', # invalid UTF8 - b'\n', - b'{"is this invalid json?": true\n', - b'{"valid json without args": true}\n', +OLD_VERSION = str(ipc.PROTOCOL_VERSION - 1).encode('utf-8') +NEW_VERSION = str(ipc.PROTOCOL_VERSION + 1).encode('utf-8') + + +@pytest.mark.parametrize('data, msg', [ + (b'\x80\n', 'invalid utf-8'), + (b'\n', 'invalid json'), + (b'{"is this invalid json?": true\n', 'invalid json'), + (b'{"valid json without args": true}\n', 'no args'), + (b'{"args": [], "protocol_version": ' + OLD_VERSION + b'}\n', + 'incompatible version'), + (b'{"args": [], "protocol_version": ' + NEW_VERSION + b'}\n', + 'incompatible version'), + (b'{"args": [], "protocol_version": "foo"}\n', 'invalid version'), + (b'{"args": []}\n', 'invalid version'), ]) -def test_invalid_data(qtbot, ipc_server, connected_socket, caplog, data): +def test_invalid_data(qtbot, ipc_server, connected_socket, caplog, data, msg): + got_args_spy = QSignalSpy(ipc_server.got_args) + signals = [ipc_server.got_invalid_data, connected_socket.disconnected] with caplog.atLevel(logging.ERROR): with qtbot.waitSignals(signals, raising=True): connected_socket.write(data) + messages = [r.message for r in caplog.records()] assert messages[-1] == 'Ignoring invalid IPC data.' + assert messages[-2].startswith(msg) + assert not got_args_spy def test_multiline(qtbot, ipc_server, connected_socket): spy = QSignalSpy(ipc_server.got_args) error_spy = QSignalSpy(ipc_server.got_invalid_data) + data = ('{{"args": ["one"], "protocol_version": {version}}}\n' + '{{"args": ["two"], "protocol_version": {version}}}\n'.format( + version=ipc.PROTOCOL_VERSION)) + with qtbot.waitSignals([ipc_server.got_args, ipc_server.got_args], raising=True): - connected_socket.write(b'{"args": ["one"]}\n{"args": ["two"]}\n') + connected_socket.write(data.encode('utf-8')) assert len(spy) == 2 assert not error_spy @@ -396,7 +417,8 @@ class TestSendToRunningInstance: assert len(raw_spy) == 1 assert len(raw_spy[0]) == 1 - raw_expected = {'args': ['foo'], 'version': qutebrowser.__version__} + raw_expected = {'args': ['foo'], 'version': qutebrowser.__version__, + 'protocol_version': ipc.PROTOCOL_VERSION} if has_cwd: raw_expected['cwd'] = str(tmpdir) parsed = json.loads(raw_spy[0][0].decode('utf-8'))