Repository: mitmproxy/mitmproxy

Issue:
Mitmproxy crashes, when trying to send cuts to the clipboard without copy/paste mechanism for system
##### Steps to reproduce the problem:

1. Run mitmproxy.
2. Press `Ctrl l`
3. Input `: cut.clip 1 2 `. Press `Enter`.

Traceback:
```
Traceback (most recent call last):
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/master.py", line 216, in run
    self.loop.run()
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 278, in run
    self._run()
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 376, in _run
    self.event_loop.run()
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 682, in run
    self._loop()
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 719, in _loop
    self._watch_files[fd]()
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/raw_display.py", line 393, in <lambda>
    event_loop, callback, self.get_available_raw_input())
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/raw_display.py", line 493, in parse_input
    callback(processed, processed_codes)
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 403, in _update
    self.process_input(keys)
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/main_loop.py", line 503, in process_input
    k = self._topmost_widget.keypress(self.screen_size, k)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/window.py", line 309, in keypress
    k = super().keypress(size, k)
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/urwid/container.py", line 1116, in keypress
    return self.footer.keypress((maxcol,),key)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/statusbar.py", line 149, in keypress
    return self.ab.keypress(*args, **kwargs)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/statusbar.py", line 104, in keypress
    self.prompt_execute(self._w.get_edit_text())
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/statusbar.py", line 124, in prompt_execute
    msg = p(txt)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/tools/console/commandexecutor.py", line 17, in __call__
    ret = self.master.commands.call(cmd)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/command.py", line 221, in call
    return self.call_args(parts[0], parts[1:])
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/command.py", line 212, in call_args
    return self.commands[path].call(args)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/command.py", line 101, in call
    ret = self.func(*pargs)
  File "/usr/lib/python3.5/contextlib.py", line 77, in __exit__
    self.gen.throw(type, value, traceback)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/master.py", line 68, in handlecontext
    yield
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/command.py", line 101, in call
    ret = self.func(*pargs)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/command.py", line 251, in wrapper
    return function(*args, **kwargs)
  File "/home/kajoj/Mitmproxy/mitmproxy/mitmproxy/addons/cut.py", line 139, in clip
    pyperclip.copy(fp.getvalue())
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/pyperclip/__init__.py", line 574, in lazy_load_stub_copy
    return copy(text)
  File "/home/kajoj/Mitmproxy/mitmproxy/venv/lib/python3.5/site-packages/pyperclip/__init__.py", line 284, in __call__
    raise PyperclipException(EXCEPT_MSG)
pyperclip.PyperclipException: 
    Pyperclip could not find a copy/paste mechanism for your system.
    For more information, please visit https://pyperclip.readthedocs.io/en/latest/introduction.html#not-implemented-error 
```

##### Any other comments? What have you tried so far?

As traceback says, the issue is relevant for Linux without installed copy/paste mechanism and can be fixed using this https://pyperclip.readthedocs.io/en/latest/introduction.html#not-implemented-error.
I think we must just handle it properly.


##### System information

Mitmproxy: 3.0.0.dev64 (commit 6dd336f) 
Python:    3.5.2
OpenSSL:   OpenSSL 1.1.0g  2 Nov 2017
Platform:  Linux-4.4.0-112-generic-x86_64-with-Ubuntu-16.04-xenial



Discussion:

Resolving patch:
diff --git a/mitmproxy/addons/cut.py b/mitmproxy/addons/cut.py
--- a/mitmproxy/addons/cut.py
+++ b/mitmproxy/addons/cut.py
@@ -139,4 +139,7 @@ def clip(
                     [strutils.always_str(v) or "" for v in vals]  # type: ignore
                 )
             ctx.log.alert("Clipped %s cuts as CSV." % len(cuts))
-        pyperclip.copy(fp.getvalue())
+        try:
+            pyperclip.copy(fp.getvalue())
+        except pyperclip.PyperclipException as e:
+            ctx.log.error(str(e))
diff --git a/mitmproxy/addons/export.py b/mitmproxy/addons/export.py
--- a/mitmproxy/addons/export.py
+++ b/mitmproxy/addons/export.py
@@ -77,4 +77,7 @@ def clip(self, fmt: str, f: flow.Flow) -> None:
             raise exceptions.CommandError("No such export format: %s" % fmt)
         func = formats[fmt]  # type: typing.Any
         v = strutils.always_str(func(f))
-        pyperclip.copy(v)
+        try:
+            pyperclip.copy(v)
+        except pyperclip.PyperclipException as e:
+            ctx.log.error(str(e))


Test patch:
diff --git a/test/mitmproxy/addons/test_cut.py b/test/mitmproxy/addons/test_cut.py
--- a/test/mitmproxy/addons/test_cut.py
+++ b/test/mitmproxy/addons/test_cut.py
@@ -7,6 +7,7 @@
 from mitmproxy.test import tflow
 from mitmproxy.test import tutils
 import pytest
+import pyperclip
 from unittest import mock
 
 
@@ -89,6 +90,13 @@ def test_cut_clip():
             tctx.command(c.clip, "@all", "request.method,request.content")
             assert pc.called
 
+        with mock.patch('pyperclip.copy') as pc:
+            log_message = "Pyperclip could not find a " \
+                          "copy/paste mechanism for your system."
+            pc.side_effect = pyperclip.PyperclipException(log_message)
+            tctx.command(c.clip, "@all", "request.method")
+            assert tctx.master.has_log(log_message, level="error")
+
 
 def test_cut_save(tmpdir):
     f = str(tmpdir.join("path"))
diff --git a/test/mitmproxy/addons/test_export.py b/test/mitmproxy/addons/test_export.py
--- a/test/mitmproxy/addons/test_export.py
+++ b/test/mitmproxy/addons/test_export.py
@@ -1,6 +1,8 @@
-import pytest
 import os
 
+import pytest
+import pyperclip
+
 from mitmproxy import exceptions
 from mitmproxy.addons import export  # heh
 from mitmproxy.test import tflow
@@ -111,7 +113,7 @@ def test_export_open(exception, log_message, tmpdir):
 
 def test_clip(tmpdir):
     e = export.Export()
-    with taddons.context():
+    with taddons.context() as tctx:
         with pytest.raises(exceptions.CommandError):
             e.clip("nonexistent", tflow.tflow(resp=True))
 
@@ -122,3 +124,10 @@ def test_clip(tmpdir):
         with mock.patch('pyperclip.copy') as pc:
             e.clip("curl", tflow.tflow(resp=True))
             assert pc.called
+
+        with mock.patch('pyperclip.copy') as pc:
+            log_message = "Pyperclip could not find a " \
+                          "copy/paste mechanism for your system."
+            pc.side_effect = pyperclip.PyperclipException(log_message)
+            e.clip("raw", tflow.tflow(resp=True))
+            assert tctx.master.has_log(log_message, level="error")