From 52716e3439ceeb47d1fe8545a4875dc36866c37c Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Thu, 16 Apr 2015 10:57:12 +1200 Subject: [PATCH] console: first pass of a Set-cookie editor for responses --- libmproxy/console/common.py | 34 +++--- libmproxy/console/flowview.py | 29 ++++- libmproxy/console/grideditor.py | 197 ++++++++++++++++++++++++-------- 3 files changed, 195 insertions(+), 65 deletions(-) diff --git a/libmproxy/console/common.py b/libmproxy/console/common.py index 23d3a4a47..74d510ebb 100644 --- a/libmproxy/console/common.py +++ b/libmproxy/console/common.py @@ -66,20 +66,26 @@ def format_keyvals(lst, key="key", val="text", indent=0): if kv is None: ret.append(urwid.Text("")) else: - cols = [] - # This cumbersome construction process is here for a reason: - # Urwid < 1.0 barfs if given a fixed size column of size zero. - if indent: - cols.append(("fixed", indent, urwid.Text(""))) - cols.extend([ - ( - "fixed", - maxk, - urwid.Text([(key, kv[0] or "")]) - ), - kv[1] if isinstance(kv[1], urwid.Widget) else urwid.Text([(val, kv[1])]) - ]) - ret.append(urwid.Columns(cols, dividechars = 2)) + if isinstance(kv[1], urwid.Widget): + v = kv[1] + elif kv[1] is None: + v = urwid.Text("") + else: + v = urwid.Text([(val, kv[1])]) + ret.append( + urwid.Columns( + [ + ("fixed", indent, urwid.Text("")), + ( + "fixed", + maxk, + urwid.Text([(key, kv[0] or "")]) + ), + v + ], + dividechars = 2 + ) + ) return ret diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index 3b1a92eca..497248de9 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -316,6 +316,19 @@ class FlowView(tabs.Tabs): conn.set_cookies(od) signals.flow_change.send(self, flow = self.flow) + def set_setcookies(self, lst, conn): + vals = [] + for i in lst: + vals.append( + [ + i[0], + [i[1], odict.ODictCaseless(i[2])] + ] + ) + od = odict.ODict(vals) + conn.set_cookies(od) + signals.flow_change.send(self, flow = self.flow) + def edit(self, part): if self.tab_offset == TAB_REQ: message = self.flow.request @@ -338,7 +351,18 @@ class FlowView(tabs.Tabs): message ) ) - pass + if message == self.flow.response and part == "c": + flattened = [] + for k, v in message.get_cookies().items(): + flattened.append([k, v[0], v[1].lst]) + self.master.view_grideditor( + grideditor.SetCookieEditor( + self.master, + flattened, + self.set_setcookies, + message + ) + ) if part == "r": with decoded(message): # Fix an issue caused by some editors when editing a @@ -541,7 +565,7 @@ class FlowView(tabs.Tabs): signals.status_prompt_onekey.send( prompt = "Edit request", keys = ( - ("cookie", "c"), + ("cookies", "c"), ("query", "q"), ("path", "p"), ("url", "u"), @@ -556,6 +580,7 @@ class FlowView(tabs.Tabs): signals.status_prompt_onekey.send( prompt = "Edit response", keys = ( + ("cookies", "c"), ("code", "o"), ("message", "m"), ("header", "h"), diff --git a/libmproxy/console/grideditor.py b/libmproxy/console/grideditor.py index 493268baf..6d112e426 100644 --- a/libmproxy/console/grideditor.py +++ b/libmproxy/console/grideditor.py @@ -7,7 +7,7 @@ import urwid from . import common, signals from .. import utils, filt, script -from netlib import http_uastrings +from netlib import http_uastrings, http_cookies FOOTER = [ @@ -19,17 +19,36 @@ FOOTER_EDITING = [ ] -class SText(urwid.WidgetWrap): - def __init__(self, txt, focused, error): +class TextColumn: + subeditor = None + + def __init__(self, heading): + self.heading = heading + + def text(self, obj): + return SEscaped(obj or "") + + def blank(self): + return "" + + +class SubgridColumn: + def __init__(self, heading, subeditor): + self.heading = heading + self.subeditor = subeditor + + def text(self, obj): + p = http_cookies._format_pairs(obj, sep="\n") + return urwid.Text(p) + + def blank(self): + return [] + + +class SEscaped(urwid.WidgetWrap): + def __init__(self, txt): txt = txt.encode("string-escape") w = urwid.Text(txt, wrap="any") - if focused: - if error: - w = urwid.AttrWrap(w, "focusfield_error") - else: - w = urwid.AttrWrap(w, "focusfield") - elif error: - w = urwid.AttrWrap(w, "field_error") urwid.WidgetWrap.__init__(self, w) def get_text(self): @@ -50,7 +69,7 @@ class SEdit(urwid.WidgetWrap): urwid.WidgetWrap.__init__(self, w) def get_text(self): - return self._w.get_text()[0] + return self._w.get_text()[0].strip() def selectable(self): return True @@ -67,9 +86,15 @@ class GridRow(urwid.WidgetWrap): self.editing = SEdit(v) self.fields.append(self.editing) else: - self.fields.append( - SText(v, True if focused == i else False, i in errors) - ) + w = self.editor.columns[i].text(v) + if focused == i: + if i in errors: + w = urwid.AttrWrap(w, "focusfield_error") + else: + w = urwid.AttrWrap(w, "focusfield") + elif i in errors: + w = urwid.AttrWrap(w, "field_error") + self.fields.append(w) fspecs = self.fields[:] if len(self.fields) > 1: @@ -126,7 +151,9 @@ class GridWalker(urwid.ListWalker): val = val.decode("string-escape") except ValueError: signals.status_message.send( - self, message = "Invalid Python-style string encoding.", expure = 1000 + self, + message = "Invalid Python-style string encoding.", + expire = 1000 ) return errors = self.lst[self.focus][1] @@ -136,10 +163,15 @@ class GridWalker(urwid.ListWalker): errors.add(self.focus_col) else: errors.discard(self.focus_col) + self.set_value(val, self.focus, self.focus_col, errors) - row = list(self.lst[self.focus][0]) - row[self.focus_col] = val - self.lst[self.focus] = [tuple(row), errors] + def set_value(self, val, focus, focus_col, errors=None): + if not errors: + errors = set([]) + row = list(self.lst[focus][0]) + row[focus_col] = val + self.lst[focus] = [tuple(row), errors] + self._modified() def delete_focus(self): if self.lst: @@ -149,7 +181,10 @@ class GridWalker(urwid.ListWalker): def _insert(self, pos): self.focus = pos - self.lst.insert(self.focus, [[""]*self.editor.columns, set([])]) + self.lst.insert( + self.focus, [ + [c.blank() for c in self.editor.columns], set([])] + ) self.focus_col = 0 self.start_edit() @@ -179,12 +214,12 @@ class GridWalker(urwid.ListWalker): self._modified() def right(self): - self.focus_col = min(self.focus_col + 1, self.editor.columns-1) + self.focus_col = min(self.focus_col + 1, len(self.editor.columns)-1) self._modified() def tab_next(self): self.stop_edit() - if self.focus_col < self.editor.columns-1: + if self.focus_col < len(self.editor.columns)-1: self.focus_col += 1 elif self.focus != len(self.lst)-1: self.focus_col = 0 @@ -231,7 +266,6 @@ FIRST_WIDTH_MIN = 20 class GridEditor(urwid.WidgetWrap): title = None columns = None - headings = None def __init__(self, master, value, callback, *cb_args, **cb_kwargs): value = copy.deepcopy(value) @@ -241,7 +275,7 @@ class GridEditor(urwid.WidgetWrap): first_width = 20 if value: for r in value: - assert len(r) == self.columns + assert len(r) == len(self.columns) first_width = max(len(r), first_width) self.first_width = min(first_width, FIRST_WIDTH_MAX) @@ -250,9 +284,9 @@ class GridEditor(urwid.WidgetWrap): title = urwid.AttrWrap(title, "heading") headings = [] - for i, h in enumerate(self.headings): - c = urwid.Text(h) - if i == 0 and len(self.headings) > 1: + for i, col in enumerate(self.columns): + c = urwid.Text(col.heading) + if i == 0 and len(self.columns) > 1: headings.append(("fixed", first_width + 2, c)) else: headings.append(c) @@ -303,6 +337,9 @@ class GridEditor(urwid.WidgetWrap): except IOError, v: return str(v) + def set_subeditor_value(self, val, focus, focus_col): + self.walker.set_value(val, focus, focus_col) + def keypress(self, size, key): if self.walker.editing: if key in ["esc"]: @@ -317,10 +354,11 @@ class GridEditor(urwid.WidgetWrap): return None key = common.shortcuts(key) + column = self.columns[self.walker.focus_col] if key in ["q", "esc"]: res = [] for i in self.walker.lst: - if not i[1] and any([x.strip() for x in i[0]]): + if not i[1] and any([x for x in i[0]]): res.append(i[0]) self.callback(res, *self.cb_args, **self.cb_kwargs) signals.pop_view_state.send(self) @@ -337,6 +375,13 @@ class GridEditor(urwid.WidgetWrap): elif key == "d": self.walker.delete_focus() elif key == "r": + if column.subeditor: + signals.status_message.send( + self, + message = "Press enter to edit this field.", + expire = 1000 + ) + return if self.walker.get_current_value() is not None: signals.status_prompt_path.send( self, @@ -344,6 +389,13 @@ class GridEditor(urwid.WidgetWrap): callback = self.read_file ) elif key == "R": + if column.subeditor: + signals.status_message.send( + self, + message = "Press enter to edit this field.", + expire = 1000 + ) + return if self.walker.get_current_value() is not None: signals.status_prompt_path.send( self, @@ -352,6 +404,13 @@ class GridEditor(urwid.WidgetWrap): args = (True,) ) elif key == "e": + if column.subeditor: + signals.status_message.send( + self, + message = "Press enter to edit this field.", + expire = 1000 + ) + return o = self.walker.get_current_value() if o is not None: n = self.master.spawn_editor(o.encode("string-escape")) @@ -359,7 +418,19 @@ class GridEditor(urwid.WidgetWrap): self.walker.set_current_value(n, False) self.walker._modified() elif key in ["enter"]: - self.walker.start_edit() + if column.subeditor: + self.master.view_grideditor( + self.columns[self.walker.focus_col].subeditor( + self.master, + self.walker.get_current_value(), + self.set_subeditor_value, + self.walker.focus, + self.walker.focus_col + ) + ) + else: + self.walker.start_edit() + elif not self.handle_key(key): return self._w.keypress(size, key) @@ -403,14 +474,18 @@ class GridEditor(urwid.WidgetWrap): class QueryEditor(GridEditor): title = "Editing query" - columns = 2 - headings = ("Key", "Value") + columns = [ + TextColumn("Key"), + TextColumn("Value") + ] class HeaderEditor(GridEditor): title = "Editing headers" - columns = 2 - headings = ("Key", "Value") + columns = [ + TextColumn("Key"), + TextColumn("Value") + ] def make_help(self): h = GridEditor.make_help(self) @@ -448,14 +523,19 @@ class HeaderEditor(GridEditor): class URLEncodedFormEditor(GridEditor): title = "Editing URL-encoded form" - columns = 2 - headings = ("Key", "Value") + columns = [ + TextColumn("Key"), + TextColumn("Value") + ] class ReplaceEditor(GridEditor): title = "Editing replacement patterns" - columns = 3 - headings = ("Filter", "Regex", "Replacement") + columns = [ + TextColumn("Filter"), + TextColumn("Regex"), + TextColumn("Replacement"), + ] def is_error(self, col, val): if col == 0: @@ -471,8 +551,11 @@ class ReplaceEditor(GridEditor): class SetHeadersEditor(GridEditor): title = "Editing header set patterns" - columns = 3 - headings = ("Filter", "Header", "Value") + columns = [ + TextColumn("Filter"), + TextColumn("Header"), + TextColumn("Value"), + ] def is_error(self, col, val): if col == 0: @@ -517,14 +600,16 @@ class SetHeadersEditor(GridEditor): class PathEditor(GridEditor): title = "Editing URL path components" - columns = 1 - headings = ("Component",) + columns = [ + TextColumn("Component"), + ] class ScriptEditor(GridEditor): title = "Editing scripts" - columns = 1 - headings = ("Command",) + columns = [ + TextColumn("Command"), + ] def is_error(self, col, val): try: @@ -535,8 +620,9 @@ class ScriptEditor(GridEditor): class HostPatternEditor(GridEditor): title = "Editing host patterns" - columns = 1 - headings = ("Regex (matched on hostname:port / ip:port)",) + columns = [ + TextColumn("Regex (matched on hostname:port / ip:port)") + ] def is_error(self, col, val): try: @@ -547,11 +633,24 @@ class HostPatternEditor(GridEditor): class CookieEditor(GridEditor): title = "Editing request Cookie header" - columns = 2 - headings = ("Name", "Value") + columns = [ + TextColumn("Name"), + TextColumn("Value"), + ] + + +class CookieAttributeEditor(GridEditor): + title = "Editing Set-Cookie attributes" + columns = [ + TextColumn("Name"), + TextColumn("Value"), + ] class SetCookieEditor(GridEditor): - title = "Editing request SetCookie header" - columns = 2 - headings = ("Name", "Value") + title = "Editing response SetCookie header" + columns = [ + TextColumn("Name"), + TextColumn("Value"), + SubgridColumn("Attributes", CookieAttributeEditor), + ]