mitmproxy/libmproxy/console/flowview.py

703 lines
24 KiB
Python
Raw Normal View History

2015-11-26 13:58:08 +00:00
from __future__ import absolute_import, division
2015-04-14 21:43:15 +00:00
import os
import traceback
2015-09-12 11:49:16 +00:00
import sys
2015-11-26 13:58:08 +00:00
import math
import urwid
from netlib import odict
2015-09-16 16:45:22 +00:00
from netlib.http import CONTENT_MISSING, Headers
from . import common, grideditor, signals, searchable, tabs
from . import flowdetailview
2015-09-12 11:49:16 +00:00
from .. import utils, controller, contentviews
2015-08-30 13:27:29 +00:00
from ..models import HTTPRequest, HTTPResponse, decoded
from ..exceptions import ContentViewException
2015-04-14 21:43:15 +00:00
class SearchError(Exception):
pass
def _mkhelp():
text = []
keys = [
("A", "accept all intercepted flows"),
("a", "accept this intercepted flow"),
("b", "save request/response body"),
2015-06-09 02:27:14 +00:00
("Z", "copy as curl command"),
("d", "delete flow"),
("D", "duplicate flow"),
("e", "edit request/response"),
2012-04-08 08:16:25 +00:00
("f", "load full body data"),
("m", "change body display mode for this entity"),
2015-05-30 00:03:28 +00:00
(None,
common.highlight_key("automatic", "a") +
[("text", ": automatic detection")]
),
(None,
common.highlight_key("hex", "e") +
[("text", ": Hex")]
),
(None,
common.highlight_key("html", "h") +
[("text", ": HTML")]
),
(None,
common.highlight_key("image", "i") +
[("text", ": Image")]
),
(None,
common.highlight_key("javascript", "j") +
[("text", ": JavaScript")]
),
(None,
common.highlight_key("json", "s") +
[("text", ": JSON")]
),
(None,
common.highlight_key("urlencoded", "u") +
[("text", ": URL-encoded data")]
),
(None,
common.highlight_key("raw", "r") +
[("text", ": raw data")]
),
(None,
common.highlight_key("xml", "x") +
[("text", ": XML")]
),
("M", "change default body display mode"),
("p", "previous flow"),
("P", "copy response(content/headers) to clipboard"),
("r", "replay request"),
("V", "revert changes to request"),
("v", "view body in external viewer"),
("w", "save all flows matching current limit"),
("W", "save this flow"),
("x", "delete body"),
("z", "encode/decode a request/response"),
("tab", "next tab"),
2015-03-31 21:34:58 +00:00
("h, l", "previous tab, next tab"),
("space", "next flow"),
("|", "run script on this flow"),
("/", "search (case sensitive)"),
2014-02-22 03:36:35 +00:00
("n", "repeat search forward"),
("N", "repeat search backwards"),
]
text.extend(common.format_keyvals(keys, key="key", val="text", indent=4))
return text
help_context = _mkhelp()
footer = [
('heading_key', "?"), ":help ",
('heading_key', "q"), ":back ",
]
class FlowViewHeader(urwid.WidgetWrap):
def __init__(self, master, f):
self.master, self.flow = master, f
2015-03-22 04:33:25 +00:00
self._w = common.format_flow(
f,
False,
extended=True,
padding=0,
hostheader=self.master.showhost
)
signals.flow_change.connect(self.sig_flow_change)
def sig_flow_change(self, sender, flow):
if flow == self.flow:
self._w = common.format_flow(
flow,
False,
extended=True,
padding=0,
hostheader=self.master.showhost
)
cache = utils.LRUCache(200)
TAB_REQ = 0
TAB_RESP = 1
2015-04-14 21:43:15 +00:00
class FlowView(tabs.Tabs):
2013-12-25 03:57:54 +00:00
highlight_color = "focusfield"
def __init__(self, master, state, flow, tab_offset):
self.master, self.state, self.flow = master, state, flow
tabs.Tabs.__init__(self,
2015-05-30 00:03:28 +00:00
[
(self.tab_request, self.view_request),
(self.tab_response, self.view_response),
(self.tab_details, self.view_details),
],
tab_offset
)
self.show()
self.last_displayed_body = None
signals.flow_change.connect(self.sig_flow_change)
def tab_request(self):
if self.flow.intercepted and not self.flow.reply.acked and not self.flow.response:
return "Request intercepted"
else:
return "Request"
def tab_response(self):
if self.flow.intercepted and not self.flow.reply.acked and self.flow.response:
return "Response intercepted"
else:
return "Response"
def tab_details(self):
return "Detail"
def view_request(self):
return self.conn_text(self.flow.request)
def view_response(self):
return self.conn_text(self.flow.response)
def view_details(self):
return flowdetailview.flowdetails(self.state, self.flow)
def sig_flow_change(self, sender, flow):
if flow == self.flow:
self.show()
2015-09-12 11:49:16 +00:00
def content_view(self, viewmode, message):
if message.content == CONTENT_MISSING:
msg, body = "", [urwid.Text([("error", "[content missing]")])]
2015-09-12 11:49:16 +00:00
return msg, body
else:
full = self.state.get_flow_setting(
self.flow,
(self.tab_offset, "fullcontents"),
False
)
if full:
2015-05-30 00:03:28 +00:00
limit = sys.maxsize
2013-12-26 04:04:18 +00:00
else:
2015-09-12 11:49:16 +00:00
limit = contentviews.VIEW_CUTOFF
2015-09-11 11:37:52 +00:00
return cache.get(
self._get_content_view,
2015-04-14 21:43:15 +00:00
viewmode,
2015-09-12 11:49:16 +00:00
message,
2015-09-19 09:50:02 +00:00
limit,
(bytes(message.headers), message.content) # Cache invalidation
2015-04-14 21:43:15 +00:00
)
2013-12-24 01:28:20 +00:00
2015-09-19 09:50:02 +00:00
def _get_content_view(self, viewmode, message, max_lines, _):
2015-09-12 11:49:16 +00:00
try:
2015-09-12 11:49:16 +00:00
description, lines = contentviews.get_content_view(
viewmode, message.content, headers=message.headers
)
except ContentViewException:
s = "Content viewer failed: \n" + traceback.format_exc()
signals.add_event(s, "error")
2015-09-12 11:49:16 +00:00
description, lines = contentviews.get_content_view(
contentviews.get("Raw"), message.content, headers=message.headers
)
2015-09-11 11:37:52 +00:00
description = description.replace("Raw", "Couldn't parse: falling back to Raw")
2015-09-12 11:49:16 +00:00
# Give hint that you have to tab for the response.
if description == "No content" and isinstance(message, HTTPRequest):
description = "No request content (press tab to view response)"
2015-11-26 13:58:08 +00:00
# If the users has a wide terminal, he gets fewer lines; this should not be an issue.
chars_per_line = 80
max_chars = max_lines * chars_per_line
total_chars = 0
2015-09-12 11:49:16 +00:00
text_objects = []
for line in lines:
2015-11-26 13:58:08 +00:00
txt = []
for (style, text) in line:
if total_chars + len(text) > max_chars:
text = text[:max_chars-total_chars]
txt.append((style, text))
total_chars += len(text)
if total_chars == max_chars:
break
# round up to the next line.
total_chars = int(math.ceil(total_chars / chars_per_line) * chars_per_line)
text_objects.append(urwid.Text(txt))
if total_chars == max_chars:
2015-09-12 11:49:16 +00:00
text_objects.append(urwid.Text([
("highlight", "Stopped displaying data after %d lines. Press " % max_lines),
("key", "f"),
("highlight", " to load all data.")
]))
break
2015-09-11 11:37:52 +00:00
return description, text_objects
2015-03-29 02:27:17 +00:00
def viewmode_get(self):
override = self.state.get_flow_setting(
self.flow,
(self.tab_offset, "prettyview")
2015-03-29 02:27:17 +00:00
)
2013-12-26 04:04:18 +00:00
return self.state.default_body_view if override is None else override
def conn_text(self, conn):
if conn:
txt = common.format_keyvals(
2015-09-05 18:45:58 +00:00
[(h + ":", v) for (h, v) in conn.headers.fields],
2015-04-14 21:43:15 +00:00
key = "header",
val = "text"
)
viewmode = self.viewmode_get()
msg, body = self.content_view(viewmode, conn)
2015-04-14 21:43:15 +00:00
cols = [
urwid.Text(
[
("heading", msg),
]
2015-09-12 11:49:16 +00:00
),
2015-04-14 21:43:15 +00:00
urwid.Text(
[
" ",
('heading', "["),
('heading_key', "m"),
2015-05-30 00:03:28 +00:00
('heading', (":%s]" % viewmode.name)),
],
align="right"
)
2015-09-12 11:49:16 +00:00
]
title = urwid.AttrWrap(urwid.Columns(cols), "heading")
2013-12-24 01:28:20 +00:00
txt.append(title)
txt.extend(body)
else:
txt = [
urwid.Text(""),
urwid.Text(
[
("highlight", "No response. Press "),
("key", "e"),
("highlight", " and edit any aspect to add one."),
]
)
]
return searchable.Searchable(self.state, txt)
def set_method_raw(self, m):
if m:
self.flow.request.method = m
signals.flow_change.send(self, flow = self.flow)
def edit_method(self, m):
if m == "e":
signals.status_prompt.send(
2015-03-22 00:59:34 +00:00
prompt = "Method",
text = self.flow.request.method,
callback = self.set_method_raw
)
else:
for i in common.METHOD_OPTIONS:
if i[1] == m:
self.flow.request.method = i[0].upper()
signals.flow_change.send(self, flow = self.flow)
def set_url(self, url):
request = self.flow.request
2014-09-03 21:44:54 +00:00
try:
request.url = str(url)
except ValueError:
return "Invalid URL."
signals.flow_change.send(self, flow = self.flow)
def set_resp_code(self, code):
response = self.flow.response
try:
response.status_code = int(code)
except ValueError:
return None
import BaseHTTPServer
2015-05-30 00:03:28 +00:00
if int(code) in BaseHTTPServer.BaseHTTPRequestHandler.responses:
response.msg = BaseHTTPServer.BaseHTTPRequestHandler.responses[
int(code)][0]
signals.flow_change.send(self, flow = self.flow)
def set_resp_msg(self, msg):
response = self.flow.response
response.msg = msg
signals.flow_change.send(self, flow = self.flow)
2015-09-05 18:45:58 +00:00
def set_headers(self, fields, conn):
conn.headers = Headers(fields)
signals.flow_change.send(self, flow = self.flow)
def set_query(self, lst, conn):
conn.set_query(odict.ODict(lst))
signals.flow_change.send(self, flow = self.flow)
def set_path_components(self, lst, conn):
conn.set_path_components(lst)
signals.flow_change.send(self, flow = self.flow)
def set_form(self, lst, conn):
conn.set_form_urlencoded(odict.ODict(lst))
signals.flow_change.send(self, flow = self.flow)
def edit_form(self, conn):
self.master.view_grideditor(
grideditor.URLEncodedFormEditor(
self.master,
conn.get_form_urlencoded().lst,
self.set_form,
conn
)
)
def edit_form_confirm(self, key, conn):
if key == "y":
self.edit_form(conn)
2015-04-14 21:43:15 +00:00
def set_cookies(self, lst, conn):
od = odict.ODict(lst)
conn.set_cookies(od)
signals.flow_change.send(self, flow = self.flow)
def set_setcookies(self, data, conn):
conn.set_cookies(data)
signals.flow_change.send(self, flow = self.flow)
def edit(self, part):
if self.tab_offset == TAB_REQ:
2014-12-23 22:14:55 +00:00
message = self.flow.request
else:
if not self.flow.response:
2014-03-10 16:01:30 +00:00
self.flow.response = HTTPResponse(
self.flow.request.http_version,
2015-09-05 18:45:58 +00:00
200, "OK", Headers(), ""
)
self.flow.response.reply = controller.DummyReply()
2014-12-23 22:14:55 +00:00
message = self.flow.response
self.flow.backup()
2015-04-14 21:43:15 +00:00
if message == self.flow.request and part == "c":
self.master.view_grideditor(
grideditor.CookieEditor(
self.master,
message.get_cookies().lst,
self.set_cookies,
message
)
)
if message == self.flow.response and part == "c":
self.master.view_grideditor(
grideditor.SetCookieEditor(
self.master,
message.get_cookies(),
self.set_setcookies,
message
)
)
if part == "r":
2014-12-23 22:14:55 +00:00
with decoded(message):
2015-03-22 04:33:25 +00:00
# Fix an issue caused by some editors when editing a
# request/response body. Many editors make it hard to save a
# file without a terminating newline on the last line. When
# editing message bodies, this can cause problems. For now, I
# just strip the newlines off the end of the body when we return
# from an editor.
2014-12-23 22:14:55 +00:00
c = self.master.spawn_editor(message.content or "")
message.content = c.rstrip("\n")
elif part == "f":
2014-12-23 22:14:55 +00:00
if not message.get_form_urlencoded() and message.content:
signals.status_prompt_onekey.send(
prompt = "Existing body is not a URL-encoded form. Clear and edit?",
keys = [
("yes", "y"),
("no", "n"),
],
callback = self.edit_form_confirm,
args = (message,)
)
else:
2014-12-23 22:14:55 +00:00
self.edit_form(message)
elif part == "h":
self.master.view_grideditor(
grideditor.HeaderEditor(
self.master,
2015-09-05 18:45:58 +00:00
message.headers.fields,
self.set_headers,
message
)
)
elif part == "p":
2014-12-23 22:14:55 +00:00
p = message.get_path_components()
2015-03-22 04:33:25 +00:00
self.master.view_grideditor(
grideditor.PathEditor(
self.master,
p,
self.set_path_components,
message
)
)
elif part == "q":
2015-03-22 04:33:25 +00:00
self.master.view_grideditor(
grideditor.QueryEditor(
self.master,
message.get_query().lst,
self.set_query, message
)
)
elif part == "u":
signals.status_prompt.send(
2015-03-22 00:59:34 +00:00
prompt = "URL",
text = message.url,
callback = self.set_url
)
elif part == "m":
signals.status_prompt_onekey.send(
prompt = "Method",
keys = common.METHOD_OPTIONS,
callback = self.edit_method
)
2015-04-14 21:43:15 +00:00
elif part == "o":
signals.status_prompt.send(
2015-03-22 00:59:34 +00:00
prompt = "Code",
text = str(message.status_code),
callback = self.set_resp_code
)
elif part == "m":
signals.status_prompt.send(
2015-03-22 00:59:34 +00:00
prompt = "Message",
text = message.msg,
callback = self.set_resp_msg
)
signals.flow_change.send(self, flow = self.flow)
2012-02-08 01:07:17 +00:00
def _view_nextprev_flow(self, np, flow):
try:
idx = self.state.view.index(flow)
except IndexError:
return
if np == "next":
new_flow, new_idx = self.state.get_next(idx)
else:
new_flow, new_idx = self.state.get_prev(idx)
if new_flow is None:
signals.status_message.send(message="No more flows!")
else:
signals.pop_view_state.send(self)
self.master.view_flow(new_flow, self.tab_offset)
2012-02-08 01:07:17 +00:00
def view_next_flow(self, flow):
return self._view_nextprev_flow("next", flow)
def view_prev_flow(self, flow):
return self._view_nextprev_flow("prev", flow)
def change_this_display_mode(self, t):
2014-11-10 16:35:28 +00:00
self.state.add_flow_setting(
self.flow,
(self.tab_offset, "prettyview"),
2015-09-12 11:49:16 +00:00
contentviews.get_by_shortcut(t)
2014-11-10 16:35:28 +00:00
)
signals.flow_change.send(self, flow = self.flow)
def delete_body(self, t):
if t == "m":
val = CONTENT_MISSING
else:
val = None
if self.tab_offset == TAB_REQ:
self.flow.request.content = val
else:
self.flow.response.content = val
signals.flow_change.send(self, flow = self.flow)
def keypress(self, size, key):
key = super(self.__class__, self).keypress(size, key)
if key == " ":
2012-02-08 01:07:17 +00:00
self.view_next_flow(self.flow)
return
key = common.shortcuts(key)
if self.tab_offset == TAB_REQ:
conn = self.flow.request
elif self.tab_offset == TAB_RESP:
conn = self.flow.response
else:
conn = None
if key in ("up", "down", "page up", "page down"):
# Why doesn't this just work??
self._w.keypress(size, key)
elif key == "a":
2014-12-23 19:33:42 +00:00
self.flow.accept_intercept(self.master)
signals.flow_change.send(self, flow = self.flow)
elif key == "A":
self.master.accept_all()
signals.flow_change.send(self, flow = self.flow)
elif key == "d":
if self.state.flow_count() == 1:
self.master.view_flowlist()
2015-05-30 00:03:28 +00:00
elif self.state.view.index(self.flow) == len(self.state.view) - 1:
self.view_prev_flow(self.flow)
else:
self.view_next_flow(self.flow)
f = self.flow
f.kill(self.master)
self.state.delete_flow(f)
elif key == "D":
f = self.master.duplicate_flow(self.flow)
self.master.view_flow(f)
signals.status_message.send(message="Duplicated.")
elif key == "p":
self.view_prev_flow(self.flow)
elif key == "r":
r = self.master.replay_request(self.flow)
if r:
signals.status_message.send(message=r)
signals.flow_change.send(self, flow = self.flow)
elif key == "V":
if not self.flow.modified():
signals.status_message.send(message="Flow not modified.")
return
self.state.revert(self.flow)
signals.flow_change.send(self, flow = self.flow)
signals.status_message.send(message="Reverted.")
elif key == "W":
2015-03-22 00:59:34 +00:00
signals.status_prompt_path.send(
prompt = "Save this flow",
callback = self.master.save_one_flow,
args = (self.flow,)
)
2015-06-09 02:27:14 +00:00
elif key == "Z":
common.copy_as_curl_command(self.flow)
elif key == "|":
2015-03-22 00:59:34 +00:00
signals.status_prompt_path.send(
prompt = "Send flow to script",
callback = self.master.run_script_once,
args = (self.flow,)
)
if not conn and key in set(list("befgmxvz")):
signals.status_message.send(
message = "Tab to the request or response",
expire = 1
)
elif conn:
if key == "b":
if self.tab_offset == TAB_REQ:
2015-04-14 21:43:15 +00:00
common.ask_save_body(
"q", self.master, self.state, self.flow
)
else:
2015-04-14 21:43:15 +00:00
common.ask_save_body(
"s", self.master, self.state, self.flow
)
elif key == "e":
if self.tab_offset == TAB_REQ:
signals.status_prompt_onekey.send(
prompt = "Edit request",
keys = (
("cookies", "c"),
("query", "q"),
("path", "p"),
("url", "u"),
("header", "h"),
("form", "f"),
("raw body", "r"),
("method", "m"),
),
callback = self.edit
)
else:
signals.status_prompt_onekey.send(
prompt = "Edit response",
keys = (
("cookies", "c"),
2015-04-14 21:43:15 +00:00
("code", "o"),
("message", "m"),
("header", "h"),
("raw body", "r"),
),
callback = self.edit
)
key = None
elif key == "f":
signals.status_message.send(message="Loading all body data...")
self.state.add_flow_setting(
self.flow,
(self.tab_offset, "fullcontents"),
True
)
signals.flow_change.send(self, flow = self.flow)
signals.status_message.send(message="")
elif key == "P":
if self.tab_offset == TAB_REQ:
scope = "q"
else:
scope = "s"
common.ask_copy_part(scope, self.flow, self.master, self.state)
elif key == "m":
2015-09-12 11:49:16 +00:00
p = list(contentviews.view_prompts)
p.insert(0, ("Clear", "C"))
signals.status_prompt_onekey.send(
self,
prompt = "Display mode",
keys = p,
callback = self.change_this_display_mode
)
key = None
elif key == "x":
signals.status_prompt_onekey.send(
prompt = "Delete body",
keys = (
("completely", "c"),
("mark as missing", "m"),
),
callback = self.delete_body
)
key = None
elif key == "v":
if conn.content:
2015-09-05 18:45:58 +00:00
t = conn.headers.get("content-type")
2015-05-30 00:03:28 +00:00
if "EDITOR" in os.environ or "PAGER" in os.environ:
self.master.spawn_external_viewer(conn.content, t)
else:
signals.status_message.send(
message = "Error! Set $EDITOR or $PAGER."
)
elif key == "z":
self.flow.backup()
2015-09-05 18:45:58 +00:00
e = conn.headers.get("content-encoding", "identity")
if e != "identity":
if not conn.decode():
2015-03-22 04:33:25 +00:00
signals.status_message.send(
message = "Could not decode - invalid data?"
)
else:
signals.status_prompt_onekey.send(
prompt = "Select encoding: ",
keys = (
("gzip", "z"),
("deflate", "d"),
),
callback = self.encode_callback,
args = (conn,)
)
signals.flow_change.send(self, flow = self.flow)
return key
def encode_callback(self, key, conn):
encoding_map = {
"z": "gzip",
"d": "deflate",
}
conn.encode(encoding_map[key])
signals.flow_change.send(self, flow = self.flow)