diff --git a/libmproxy/console/common.py b/libmproxy/console/common.py index 3e6e5ccc5..e4a4acba3 100644 --- a/libmproxy/console/common.py +++ b/libmproxy/console/common.py @@ -1,9 +1,14 @@ from __future__ import absolute_import import urwid import urwid.util +import os from .. import utils -from ..protocol.http import CONTENT_MISSING +from ..protocol.http import CONTENT_MISSING, decoded +try: + import pyperclip +except: + pyperclip = False VIEW_LIST = 0 VIEW_FLOW = 1 @@ -162,6 +167,146 @@ def raw_format_flow(f, focus, extended, padding): return urwid.Pile(pile) +# Save file to disk +def save_data(path, data, master, state): + if not path: + return + state.last_saveload = path + path = os.path.expanduser(path) + try: + with file(path, "wb") as f: + f.write(data) + except IOError, v: + master.statusbar.message(v.strerror) + + +def ask_save_path(prompt, data, master, state): + master.path_prompt( + prompt, + state.last_saveload, + save_data, + data, + master, + state + ) + + +def copy_flow_format_data(part, scope, flow): + if part == "u": + data = flow.request.url + else: + data = "" + if scope in ("q", "a"): + with decoded(flow.request): + if part == "h": + data += flow.request.assemble() + elif part == "c": + data += flow.request.content + else: + raise ValueError("Unknown part: {}".format(part)) + if scope == "a" and flow.request.content and flow.response: + # Add padding between request and response + data += "\r\n" * 2 + if scope in ("s", "a") and flow.response: + with decoded(flow.response): + if part == "h": + data += flow.response.assemble() + elif part == "c": + data += flow.response.content + else: + raise ValueError("Unknown part: {}".format(part)) + return data + + +def copy_flow(part, scope, flow, master, state): + """ + part: _c_ontent, _a_ll, _u_rl + scope: _a_ll, re_q_uest, re_s_ponse + """ + data = copy_flow_format_data(part, scope, flow) + + if not data: + if scope == "q": + master.statusbar.message("No request content to copy.") + elif scope == "s": + master.statusbar.message("No response content to copy.") + else: + master.statusbar.message("No contents to copy.") + return + + try: + master.add_event(str(len(data))) + pyperclip.copy(data) + except RuntimeError: + def save(k): + if k == "y": + ask_save_path("Save data: ", data, master, state) + + master.prompt_onekey( + "Cannot copy binary data to clipboard. Save as file?", + ( + ("yes", "y"), + ("no", "n"), + ), + save + ) + + +def ask_copy_part(scope, flow, master, state): + choices = [ + ("content", "c"), + ("headers+content", "h") + ] + if scope != "s": + choices.append(("url", "u")) + + master.prompt_onekey( + "Copy", + choices, + copy_flow, + scope, + flow, + master, + state + ) + + +def ask_save_body(part, master, state, flow): + """ + Save either the request or the response body to disk. + part can either be "q" (request), "s" (response) or None (ask user if necessary). + """ + + request_has_content = flow.request and flow.request.content + response_has_content = flow.response and flow.response.content + + if part is None: + # We first need to determine whether we want to save the request or the response content. + if request_has_content and response_has_content: + master.prompt_onekey( + "Save", + ( + ("request", "q"), + ("response", "s"), + ), + ask_save_body, + master, + state, + flow + ) + elif response_has_content: + ask_save_body("s", master, state, flow) + else: + ask_save_body("q", master, state, flow) + + elif part == "q" and request_has_content: + ask_save_path("Save request content: ", flow.request.get_decoded_content(), master, state) + elif part == "s" and response_has_content: + ask_save_path("Save response content: ", flow.response.get_decoded_content(), master, state) + else: + master.statusbar.message("No content to save.") + + class FlowCache: @utils.LRUCache(200) def format_flow(self, *args): @@ -211,7 +356,6 @@ def format_flow(f, focus, extended=False, hostheader=False, padding=2): return flowcache.format_flow(tuple(sorted(d.items())), focus, extended, padding) - def int_version(v): SIG = 3 v = urwid.__version__.split("-")[0].split(".") diff --git a/libmproxy/console/flowlist.py b/libmproxy/console/flowlist.py index 102fa7b9f..c5cef0617 100644 --- a/libmproxy/console/flowlist.py +++ b/libmproxy/console/flowlist.py @@ -7,11 +7,13 @@ def _mkhelp(): keys = [ ("A", "accept all intercepted flows"), ("a", "accept this intercepted flow"), + ("b", "save request/response body"), ("C", "clear flow list or eventlog"), ("d", "delete flow"), ("D", "duplicate flow"), ("e", "toggle eventlog"), ("F", "toggle follow flow list"), + ("g", "copy flow to clipboard"), ("l", "set limit filter pattern"), ("L", "load saved flows"), ("r", "replay request"), @@ -204,6 +206,10 @@ class ConnectionItem(common.WWrap): self.master.run_script_once, self.flow ) + elif key == "g": + common.ask_copy_part("a", self.flow, self.master, self.state) + elif key == "b": + common.ask_save_body(None, self.master, self.state, self.flow) else: return key diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index 2aac575dd..d5d41f7bb 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -19,6 +19,7 @@ def _mkhelp(): ("D", "duplicate flow"), ("e", "edit request/response"), ("f", "load full body data"), + ("g", "copy response(content/headers) to clipboard"), ("m", "change body display mode for this entity"), (None, common.highlight_key("automatic", "a") + @@ -508,22 +509,6 @@ class FlowView(common.WWrap): self.flow.request.method = i[0].upper() self.master.refresh_flow(self.flow) - def save_body(self, path): - if not path: - return - self.state.last_saveload = path - if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: - c = self.flow.request - else: - c = self.flow.response - path = os.path.expanduser(path) - try: - f = file(path, "wb") - f.write(str(c.content)) - f.close() - except IOError, v: - self.master.statusbar.message(v.strerror) - def set_url(self, url): request = self.flow.request try: @@ -689,19 +674,10 @@ class FlowView(common.WWrap): self.master.accept_all() self.master.view_flow(self.flow) elif key == "b": - if conn: - if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: - self.master.path_prompt( - "Save request body: ", - self.state.last_saveload, - self.save_body - ) - else: - self.master.path_prompt( - "Save response body: ", - self.state.last_saveload, - self.save_body - ) + if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: + common.ask_save_body("q", self.master, self.state, self.flow) + else: + common.ask_save_body("s", self.master, self.state, self.flow) elif key == "d": if self.state.flow_count() == 1: self.master.view_flowlist() @@ -752,6 +728,12 @@ class FlowView(common.WWrap): ) self.master.refresh_flow(self.flow) self.master.statusbar.message("") + elif key == "g": + if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: + scope = "q" + else: + scope = "s" + common.ask_copy_part(scope, self.flow, self.master, self.state) elif key == "m": p = list(contentview.view_prompts) p.insert(0, ("Clear", "C")) diff --git a/setup.py b/setup.py index fde6ff66c..9e22a039d 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,8 @@ deps = { "pyasn1>0.1.2", "pyOpenSSL>=0.14", "tornado>=4.0.2", - "configargparse>=0.9.3" + "configargparse>=0.9.3", + "pyperclip>=1.5.8" } script_deps = { "mitmproxy": {