diff options
-rw-r--r-- | .travis.yml | 4 | ||||
-rw-r--r-- | mitmproxy/console/__init__.py | 20 | ||||
-rw-r--r-- | mitmproxy/controller.py | 16 | ||||
-rw-r--r-- | mitmproxy/flow.py | 3 | ||||
-rw-r--r-- | mitmproxy/proxy/root_context.py | 1 | ||||
-rw-r--r-- | mitmproxy/web/__init__.py | 9 | ||||
-rw-r--r-- | netlib/utils.py | 14 | ||||
-rw-r--r-- | pathod/app.py | 4 | ||||
-rw-r--r-- | pathod/language/__init__.py | 6 | ||||
-rw-r--r-- | pathod/language/base.py | 16 | ||||
-rw-r--r-- | pathod/log.py | 4 | ||||
-rw-r--r-- | pathod/pathoc.py | 6 | ||||
-rw-r--r-- | pathod/pathod.py | 6 | ||||
-rw-r--r-- | pathod/utils.py | 19 | ||||
-rw-r--r-- | test/mitmproxy/test_flow.py | 5 | ||||
-rw-r--r-- | test/netlib/test_utils.py | 11 | ||||
-rw-r--r-- | test/pathod/test_language_base.py | 2 | ||||
-rw-r--r-- | test/pathod/test_utils.py | 13 | ||||
-rw-r--r-- | web/src/js/components/common.js | 28 | ||||
-rw-r--r-- | web/src/js/components/eventlog.js | 4 | ||||
-rw-r--r-- | web/src/js/components/flowview/contentview.js | 64 | ||||
-rw-r--r-- | web/src/js/components/flowview/index.js | 6 | ||||
-rw-r--r-- | web/src/js/components/header.js | 28 | ||||
-rw-r--r-- | web/src/js/components/mainview.js | 13 | ||||
-rw-r--r-- | web/src/js/components/proxyapp.js | 57 |
25 files changed, 204 insertions, 155 deletions
diff --git a/.travis.yml b/.travis.yml index 7d3fbee8..4a01174a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,9 +22,9 @@ matrix: git: depth: 9999999 - python: 3.5 - env: SCOPE="netlib ./test/mitmproxy/script" + env: SCOPE="netlib ./test/mitmproxy/script ./test/pathod/test_utils.py" - python: 3.5 - env: SCOPE="netlib ./test/mitmproxy/script" NO_ALPN=1 + env: SCOPE="netlib ./test/mitmproxy/script ./test/pathod/test_utils.py" NO_ALPN=1 - python: 2.7 env: DOCS=1 script: 'cd docs && make html' diff --git a/mitmproxy/console/__init__.py b/mitmproxy/console/__init__.py index ce202b39..a87e691e 100644 --- a/mitmproxy/console/__init__.py +++ b/mitmproxy/console/__init__.py @@ -713,14 +713,15 @@ class ConsoleMaster(flow.FlowMaster): ) def process_flow(self, f): - if self.state.intercept and f.match(self.state.intercept) and not f.request.is_replay: + should_intercept = any( + [ + self.state.intercept and f.match(self.state.intercept) and not f.request.is_replay, + f.intercepted, + ] + ) + if should_intercept: f.intercept(self) - else: - # check if flow was intercepted within an inline script by flow.intercept() - if f.intercepted: - f.intercept(self) - else: - f.reply() + f.reply.take() signals.flowlist_change.send(self) signals.flow_change.send(self, flow = f) @@ -728,24 +729,29 @@ class ConsoleMaster(flow.FlowMaster): self.eventlist[:] = [] # Handlers + @controller.handler def handle_error(self, f): f = flow.FlowMaster.handle_error(self, f) if f: self.process_flow(f) return f + @controller.handler def handle_request(self, f): f = flow.FlowMaster.handle_request(self, f) + self.add_event(f.reply.acked, "info") if f: self.process_flow(f) return f + @controller.handler def handle_response(self, f): f = flow.FlowMaster.handle_response(self, f) if f: self.process_flow(f) return f + @controller.handler def handle_script_change(self, script): if super(ConsoleMaster, self).handle_script_change(script): signals.status_message.send(message='"{}" reloaded.'.format(script.filename)) diff --git a/mitmproxy/controller.py b/mitmproxy/controller.py index c125ee4f..57c01f59 100644 --- a/mitmproxy/controller.py +++ b/mitmproxy/controller.py @@ -125,7 +125,6 @@ class Channel(object): if g == exceptions.Kill: raise exceptions.Kill() return g - raise exceptions.Kill() def tell(self, mtype, m): @@ -146,6 +145,7 @@ class DummyReply(object): def __init__(self): self.acked = False self.taken = False + self.handled = False def take(self): self.taken = True @@ -164,8 +164,16 @@ def handler(f): message = args[-1] if not hasattr(message, "reply"): raise ControlError("Message %s has no reply attribute"%message) + + handling = False + # We're the first handler - ack responsibility is ours + if not message.reply.handled: + handling = True + message.reply.handled = True + ret = f(*args, **kwargs) - if not message.reply.acked and not message.reply.taken: + + if handling and not message.reply.acked and not message.reply.taken: message.reply() return ret wrapper.func_dict["handler"] = True @@ -181,8 +189,12 @@ class Reply(object): def __init__(self, obj): self.obj = obj self.q = queue.Queue() + # Has this message been acked? self.acked = False + # Has the user taken responsibility for ack-ing? self.taken = False + # Has a handler taken responsibility for ack-ing? + self.handled = False def take(self): self.taken = True diff --git a/mitmproxy/flow.py b/mitmproxy/flow.py index 2a324993..ba8dec5d 100644 --- a/mitmproxy/flow.py +++ b/mitmproxy/flow.py @@ -546,7 +546,8 @@ class FlowStore(FlowList): def kill_all(self, master): for f in self._list: - f.kill(master) + if not f.reply.acked: + f.kill(master) class State(object): diff --git a/mitmproxy/proxy/root_context.py b/mitmproxy/proxy/root_context.py index 96e7aab6..9b4e2963 100644 --- a/mitmproxy/proxy/root_context.py +++ b/mitmproxy/proxy/root_context.py @@ -132,7 +132,6 @@ class RootContext(object): class Log(object): - def __init__(self, msg, level="info"): self.msg = msg self.level = level diff --git a/mitmproxy/web/__init__.py b/mitmproxy/web/__init__.py index 956d221d..d90830d6 100644 --- a/mitmproxy/web/__init__.py +++ b/mitmproxy/web/__init__.py @@ -6,7 +6,7 @@ import sys from netlib.http import authentication -from .. import flow +from .. import flow, controller from ..exceptions import FlowReadException from . import app @@ -194,21 +194,24 @@ class WebMaster(flow.FlowMaster): if self.state.intercept and self.state.intercept( f) and not f.request.is_replay: f.intercept(self) - else: - f.reply() + f.reply.take() + @controller.handler def handle_request(self, f): super(WebMaster, self).handle_request(f) self._process_flow(f) + @controller.handler def handle_response(self, f): super(WebMaster, self).handle_response(f) self._process_flow(f) + @controller.handler def handle_error(self, f): super(WebMaster, self).handle_error(f) self._process_flow(f) + @controller.handler def add_event(self, e, level="info"): super(WebMaster, self).add_event(e, level) self.state.add_event(e, level) diff --git a/netlib/utils.py b/netlib/utils.py index 7499f71f..648915fa 100644 --- a/netlib/utils.py +++ b/netlib/utils.py @@ -425,6 +425,10 @@ def safe_subn(pattern, repl, target, *args, **kwargs): def bytes_to_escaped_str(data): """ Take bytes and return a safe string that can be displayed to the user. + + Single quotes are always escaped, double quotes are never escaped: + "'" + bytes_to_escaped_str(...) + "'" + gives a valid Python string. """ # TODO: We may want to support multi-byte characters without escaping them. # One way to do would be calling .decode("utf8", "backslashreplace") first @@ -432,17 +436,23 @@ def bytes_to_escaped_str(data): if not isinstance(data, bytes): raise ValueError("data must be bytes") - return repr(data).lstrip("b")[1:-1] + # We always insert a double-quote here so that we get a single-quoted string back + # https://stackoverflow.com/questions/29019340/why-does-python-use-different-quotes-for-representing-strings-depending-on-their + return repr(b'"' + data).lstrip("b")[2:-1] def escaped_str_to_bytes(data): """ Take an escaped string and return the unescaped bytes equivalent. """ - if not isinstance(data, str): + if not isinstance(data, six.string_types): + if six.PY2: + raise ValueError("data must be str or unicode") raise ValueError("data must be str") if six.PY2: + if isinstance(data, unicode): + data = data.encode("utf8") return data.decode("string-escape") # This one is difficult - we use an undocumented Python API here diff --git a/pathod/app.py b/pathod/app.py index aa00ed69..7e9860b9 100644 --- a/pathod/app.py +++ b/pathod/app.py @@ -1,6 +1,6 @@ import logging import pprint -from six.moves import cStringIO as StringIO +import io import copy from flask import Flask, jsonify, render_template, request, abort, make_response from . import version, language, utils @@ -145,7 +145,7 @@ def make_app(noapi, debug): args["marked"] = v.marked() return render(template, False, **args) - s = StringIO() + s = io.BytesIO() settings = copy.copy(app.config["pathod"].settings) settings.request_host = EXAMPLE_HOST diff --git a/pathod/language/__init__.py b/pathod/language/__init__.py index 32199e08..10da93ba 100644 --- a/pathod/language/__init__.py +++ b/pathod/language/__init__.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import itertools import time @@ -5,8 +7,8 @@ import pyparsing as pp from . import http, http2, websockets, writer, exceptions -from exceptions import * -from base import Settings +from .exceptions import * +from .base import Settings assert Settings # prevent pyflakes from messing with this diff --git a/pathod/language/base.py b/pathod/language/base.py index a4302998..54ca6492 100644 --- a/pathod/language/base.py +++ b/pathod/language/base.py @@ -3,9 +3,13 @@ import os import abc import pyparsing as pp +from six.moves import reduce +from netlib.utils import escaped_str_to_bytes, bytes_to_escaped_str + from .. import utils from . import generators, exceptions + class Settings(object): def __init__( @@ -105,7 +109,7 @@ class Token(object): class _TokValueLiteral(Token): def __init__(self, val): - self.val = val.decode("string_escape") + self.val = escaped_str_to_bytes(val) def get_generator(self, settings_): return self.val @@ -130,7 +134,7 @@ class TokValueLiteral(_TokValueLiteral): return v def spec(self): - inner = self.val.encode("string_escape") + inner = bytes_to_escaped_str(self.val) inner = inner.replace(r"\'", r"\x27") return "'" + inner + "'" @@ -143,7 +147,7 @@ class TokValueNakedLiteral(_TokValueLiteral): return e.setParseAction(lambda x: cls(*x)) def spec(self): - return self.val.encode("string_escape") + return bytes_to_escaped_str(self.val) class TokValueGenerate(Token): @@ -161,7 +165,7 @@ class TokValueGenerate(Token): def freeze(self, settings): g = self.get_generator(settings) - return TokValueLiteral(g[:].encode("string_escape")) + return TokValueLiteral(bytes_to_escaped_str(g[:])) @classmethod def expr(cls): @@ -221,7 +225,7 @@ class TokValueFile(Token): return generators.FileGenerator(s) def spec(self): - return "<'%s'" % self.path.encode("string_escape") + return "<'%s'" % bytes_to_escaped_str(self.path) TokValue = pp.MatchFirst( @@ -573,4 +577,4 @@ class NestedMessage(Token): def freeze(self, settings): f = self.parsed.freeze(settings).spec() - return self.__class__(TokValueLiteral(f.encode("string_escape"))) + return self.__class__(TokValueLiteral(bytes_to_escaped_str(f))) diff --git a/pathod/log.py b/pathod/log.py index f203542f..3f6aaea0 100644 --- a/pathod/log.py +++ b/pathod/log.py @@ -1,5 +1,7 @@ import datetime +import six + import netlib.utils import netlib.tcp import netlib.http @@ -53,7 +55,7 @@ class LogCtx(object): ] ) if exc_value: - raise exc_type, exc_value, traceback + six.reraise(exc_type, exc_value, traceback) def suppress(self): self.suppressed = True diff --git a/pathod/pathoc.py b/pathod/pathoc.py index a49ed351..8706868b 100644 --- a/pathod/pathoc.py +++ b/pathod/pathoc.py @@ -13,14 +13,12 @@ import threading import OpenSSL.crypto import six -from netlib import tcp, http, certutils, websockets, socks +from netlib import tcp, certutils, websockets, socks from netlib.exceptions import HttpException, TcpDisconnect, TcpTimeout, TlsException, TcpException, \ NetlibException from netlib.http import http1, http2 -import language.http -import language.websockets -from . import utils, log +from . import utils, log, language import logging from netlib.tutils import treq diff --git a/pathod/pathod.py b/pathod/pathod.py index 017ce072..af5f9e6a 100644 --- a/pathod/pathod.py +++ b/pathod/pathod.py @@ -6,15 +6,11 @@ import sys import threading import urllib -from netlib import tcp, http, certutils, websockets +from netlib import tcp, certutils, websockets from netlib.exceptions import HttpException, HttpReadDisconnect, TcpTimeout, TcpDisconnect, \ TlsException from . import version, app, language, utils, log, protocols -import language.http -import language.actions -import language.exceptions -import language.websockets DEFAULT_CERT_DOMAIN = "pathod.net" diff --git a/pathod/utils.py b/pathod/utils.py index d1e2dd00..8c6d6290 100644 --- a/pathod/utils.py +++ b/pathod/utils.py @@ -2,6 +2,8 @@ import os import sys import netlib.utils +from netlib.utils import bytes_to_escaped_str + SIZE_UNITS = dict( b=1024 ** 0, @@ -53,24 +55,13 @@ def xrepr(s): return repr(s)[1:-1] -def inner_repr(s): - """ - Returns the inner portion of a string or unicode repr (i.e. without the - quotes) - """ - if isinstance(s, unicode): - return repr(s)[2:-1] - else: - return repr(s)[1:-1] - - def escape_unprintables(s): """ Like inner_repr, but preserves line breaks. """ - s = s.replace("\r\n", "PATHOD_MARKER_RN") - s = s.replace("\n", "PATHOD_MARKER_N") - s = inner_repr(s) + s = s.replace(b"\r\n", b"PATHOD_MARKER_RN") + s = s.replace(b"\n", b"PATHOD_MARKER_N") + s = bytes_to_escaped_str(s) s = s.replace("PATHOD_MARKER_RN", "\n") s = s.replace("PATHOD_MARKER_N", "\n") return s diff --git a/test/mitmproxy/test_flow.py b/test/mitmproxy/test_flow.py index 62f23ac8..da8b8ddd 100644 --- a/test/mitmproxy/test_flow.py +++ b/test/mitmproxy/test_flow.py @@ -470,10 +470,7 @@ class TestFlow(object): fm = flow.FlowMaster(None, s) f = tutils.tflow() - fm.handle_request(f) - - f = tutils.tflow() - fm.handle_request(f) + f.intercept(fm) s.killall(fm) for i in s.view: diff --git a/test/netlib/test_utils.py b/test/netlib/test_utils.py index 1d8f7b0f..fce1d0a7 100644 --- a/test/netlib/test_utils.py +++ b/test/netlib/test_utils.py @@ -178,10 +178,15 @@ def test_bytes_to_escaped_str(): assert utils.bytes_to_escaped_str(b"\b") == r"\x08" assert utils.bytes_to_escaped_str(br"&!?=\)") == r"&!?=\\)" assert utils.bytes_to_escaped_str(b'\xc3\xbc') == r"\xc3\xbc" + assert utils.bytes_to_escaped_str(b"'") == r"\'" + assert utils.bytes_to_escaped_str(b'"') == r'"' def test_escaped_str_to_bytes(): assert utils.escaped_str_to_bytes("foo") == b"foo" - assert utils.escaped_str_to_bytes(r"\x08") == b"\b" - assert utils.escaped_str_to_bytes(r"&!?=\\)") == br"&!?=\)" - assert utils.escaped_str_to_bytes(r"ü") == b'\xc3\xbc' + assert utils.escaped_str_to_bytes("\x08") == b"\b" + assert utils.escaped_str_to_bytes("&!?=\\\\)") == br"&!?=\)" + assert utils.escaped_str_to_bytes("ü") == b'\xc3\xbc' + assert utils.escaped_str_to_bytes(u"\\x08") == b"\b" + assert utils.escaped_str_to_bytes(u"&!?=\\\\)") == br"&!?=\)" + assert utils.escaped_str_to_bytes(u"ü") == b'\xc3\xbc'
\ No newline at end of file diff --git a/test/pathod/test_language_base.py b/test/pathod/test_language_base.py index 64d4af1f..2e5d9041 100644 --- a/test/pathod/test_language_base.py +++ b/test/pathod/test_language_base.py @@ -67,7 +67,7 @@ class TestTokValueLiteral: def test_roundtrip(self): self.roundtrip("'") - self.roundtrip('\'') + self.roundtrip(r"\'") self.roundtrip("a") self.roundtrip("\"") # self.roundtrip("\\") diff --git a/test/pathod/test_utils.py b/test/pathod/test_utils.py index 4dcedf6e..8026a576 100644 --- a/test/pathod/test_utils.py +++ b/test/pathod/test_utils.py @@ -1,6 +1,8 @@ from pathod import utils import tutils +import six + def test_membool(): m = utils.MemBool() @@ -27,13 +29,10 @@ def test_data_path(): tutils.raises(ValueError, utils.data.path, "nonexistent") -def test_inner_repr(): - assert utils.inner_repr("\x66") == "\x66" - assert utils.inner_repr(u"foo") == "foo" - - def test_escape_unprintables(): - s = "".join([chr(i) for i in range(255)]) + s = bytes(range(256)) + if six.PY2: + s = "".join([chr(i) for i in range(255)]) e = utils.escape_unprintables(s) assert e.encode('ascii') - assert not "PATHOD_MARKER" in e + assert "PATHOD_MARKER" not in e diff --git a/web/src/js/components/common.js b/web/src/js/components/common.js index 21ca454f..b257b82c 100644 --- a/web/src/js/components/common.js +++ b/web/src/js/components/common.js @@ -2,32 +2,6 @@ import React from "react" import ReactDOM from "react-dom" import _ from "lodash" -export var Router = { - contextTypes: { - location: React.PropTypes.object, - router: React.PropTypes.object.isRequired - }, - updateLocation: function (pathname, queryUpdate) { - if (pathname === undefined) { - pathname = this.context.location.pathname; - } - var query = this.context.location.query; - if (queryUpdate !== undefined) { - for (var i in queryUpdate) { - if (queryUpdate.hasOwnProperty(i)) { - query[i] = queryUpdate[i] || undefined; //falsey values shall be removed. - } - } - } - this.context.router.replace({pathname, query}); - }, - getQuery: function () { - // For whatever reason, react-router always returns the same object, which makes comparing - // the current props with nextProps impossible. As a workaround, we just clone the query object. - return _.clone(this.context.location.query); - } -}; - export var Splitter = React.createClass({ getDefaultProps: function () { return { @@ -143,4 +117,4 @@ export const ToggleComponent = (props) => ToggleComponent.propTypes = { name: React.PropTypes.string.isRequired, onToggleChanged: React.PropTypes.func.isRequired -}
\ No newline at end of file +} diff --git a/web/src/js/components/eventlog.js b/web/src/js/components/eventlog.js index d1b23ace..6e4f9096 100644 --- a/web/src/js/components/eventlog.js +++ b/web/src/js/components/eventlog.js @@ -1,7 +1,6 @@ import React from "react" import ReactDOM from "react-dom" import shallowEqual from "shallowequal" -import {Router} from "./common.js" import {Query} from "../actions.js" import AutoScroll from "./helpers/AutoScroll"; import {calcVScroll} from "./helpers/VirtualScroll" @@ -144,7 +143,6 @@ function ToggleFilter ({ name, active, toggleLevel }) { const AutoScrollEventLog = AutoScroll(EventLogContents); var EventLog = React.createClass({ - mixins: [Router], getInitialState() { return { filter: { @@ -157,7 +155,7 @@ var EventLog = React.createClass({ close() { var d = {}; d[Query.SHOW_EVENTLOG] = undefined; - this.updateLocation(undefined, d); + this.props.updateLocation(undefined, d); }, toggleLevel(level) { var filter = _.extend({}, this.state.filter); diff --git a/web/src/js/components/flowview/contentview.js b/web/src/js/components/flowview/contentview.js index 2743eec3..cbac9a75 100644 --- a/web/src/js/components/flowview/contentview.js +++ b/web/src/js/components/flowview/contentview.js @@ -4,11 +4,15 @@ import _ from "lodash"; import {MessageUtils} from "../../flow/utils.js"; import {formatSize} from "../../utils.js"; -var image_regex = /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i; var ViewImage = React.createClass({ + propTypes: { + flow: React.PropTypes.object.isRequired, + message: React.PropTypes.object.isRequired, + }, statics: { + regex: /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i, matches: function (message) { - return image_regex.test(MessageUtils.getContentType(message)); + return ViewImage.regex.test(MessageUtils.getContentType(message)); } }, render: function () { @@ -19,7 +23,11 @@ var ViewImage = React.createClass({ } }); -var RawMixin = { +var ContentLoader = React.createClass({ + propTypes: { + flow: React.PropTypes.object.isRequired, + message: React.PropTypes.object.isRequired, + }, getInitialState: function () { return { content: undefined, @@ -66,41 +74,54 @@ var RawMixin = { <i className="fa fa-spinner fa-spin"></i> </div>; } - return this.renderContent(); + return React.cloneElement(this.props.children, { + content: this.state.content + }) } -}; +}); var ViewRaw = React.createClass({ - mixins: [RawMixin], + propTypes: { + content: React.PropTypes.string.isRequired, + }, statics: { + textView: true, matches: function (message) { return true; } }, - renderContent: function () { - return <pre>{this.state.content}</pre>; + render: function () { + return <pre>{this.props.content}</pre>; } }); -var json_regex = /^application\/json$/i; var ViewJSON = React.createClass({ - mixins: [RawMixin], + propTypes: { + content: React.PropTypes.string.isRequired, + }, statics: { + textView: true, + regex: /^application\/json$/i, matches: function (message) { - return json_regex.test(MessageUtils.getContentType(message)); + return ViewJSON.regex.test(MessageUtils.getContentType(message)); } }, - renderContent: function () { - var json = this.state.content; + render: function () { + var json = this.props.content; try { json = JSON.stringify(JSON.parse(json), null, 2); } catch (e) { + // @noop } return <pre>{json}</pre>; } }); var ViewAuto = React.createClass({ + propTypes: { + message: React.PropTypes.object.isRequired, + flow: React.PropTypes.object.isRequired, + }, statics: { matches: function () { return false; // don't match itself @@ -115,14 +136,18 @@ var ViewAuto = React.createClass({ } }, render: function () { + var { message, flow } = this.props var View = ViewAuto.findView(this.props.message); - return <View {...this.props}/>; + if (View.textView) { + return <ContentLoader message={message} flow={flow}><View content="" /></ContentLoader> + } else { + return <View message={message} flow={flow} /> + } } }); var all = [ViewAuto, ViewImage, ViewJSON, ViewRaw]; - var ContentEmpty = React.createClass({ render: function () { var message_name = this.props.flow.request === this.props.message ? "request" : "response"; @@ -210,6 +235,7 @@ var ContentView = React.createClass({ } }, render: function () { + var { flow, message } = this.props var message = this.props.message; if (message.contentLength === 0) { return <ContentEmpty {...this.props}/>; @@ -222,7 +248,11 @@ var ContentView = React.createClass({ var downloadUrl = MessageUtils.getContentURL(this.props.flow, message); return <div> - <this.state.View {...this.props} /> + {this.state.View.textView ? ( + <ContentLoader flow={flow} message={message}><this.state.View content="" /></ContentLoader> + ) : ( + <this.state.View flow={flow} message={message} /> + )} <div className="view-options text-center"> <ViewSelector selectView={this.selectView} active={this.state.View} message={message}/> @@ -234,4 +264,4 @@ var ContentView = React.createClass({ } }); -export default ContentView;
\ No newline at end of file +export default ContentView; diff --git a/web/src/js/components/flowview/index.js b/web/src/js/components/flowview/index.js index 47531f58..6f4f7395 100644 --- a/web/src/js/components/flowview/index.js +++ b/web/src/js/components/flowview/index.js @@ -1,6 +1,5 @@ import React from "react"; -import {Router, StickyHeadMixin} from "../common.js" import Nav from "./nav.js"; import {Request, Response, Error} from "./messages.js"; import Details from "./details.js"; @@ -15,7 +14,6 @@ var allTabs = { }; var FlowView = React.createClass({ - mixins: [StickyHeadMixin, Router], getInitialState: function () { return { prompt: false @@ -39,7 +37,7 @@ var FlowView = React.createClass({ this.selectTab(tabs[nextIndex]); }, selectTab: function (panel) { - this.updateLocation(`/flows/${this.props.flow.id}/${panel}`); + this.props.updateLocation(`/flows/${this.props.flow.id}/${panel}`); }, promptEdit: function () { var options; @@ -114,4 +112,4 @@ var FlowView = React.createClass({ } }); -export default FlowView;
\ No newline at end of file +export default FlowView; diff --git a/web/src/js/components/header.js b/web/src/js/components/header.js index 226cb61f..555babbb 100644 --- a/web/src/js/components/header.js +++ b/web/src/js/components/header.js @@ -4,7 +4,7 @@ import $ from "jquery"; import Filt from "../filt/filt.js"; import {Key} from "../utils.js"; -import {Router, ToggleComponent} from "./common.js"; +import {ToggleComponent} from "./common.js"; import {SettingsActions, FlowActions} from "../actions.js"; import {Query} from "../actions.js"; @@ -161,7 +161,6 @@ var FilterInput = React.createClass({ }); export var MainMenu = React.createClass({ - mixins: [Router], propTypes: { settings: React.PropTypes.object.isRequired, }, @@ -172,19 +171,19 @@ export var MainMenu = React.createClass({ onSearchChange: function (val) { var d = {}; d[Query.SEARCH] = val; - this.updateLocation(undefined, d); + this.props.updateLocation(undefined, d); }, onHighlightChange: function (val) { var d = {}; d[Query.HIGHLIGHT] = val; - this.updateLocation(undefined, d); + this.props.updateLocation(undefined, d); }, onInterceptChange: function (val) { SettingsActions.update({intercept: val}); }, render: function () { - var search = this.getQuery()[Query.SEARCH] || ""; - var highlight = this.getQuery()[Query.HIGHLIGHT] || ""; + var search = this.props.query[Query.SEARCH] || ""; + var highlight = this.props.query[Query.HIGHLIGHT] || ""; var intercept = this.props.settings.intercept || ""; return ( @@ -224,20 +223,19 @@ var ViewMenu = React.createClass({ title: "View", route: "flows" }, - mixins: [Router], toggleEventLog: function () { var d = {}; - if (this.getQuery()[Query.SHOW_EVENTLOG]) { + if (this.props.query[Query.SHOW_EVENTLOG]) { d[Query.SHOW_EVENTLOG] = undefined; } else { d[Query.SHOW_EVENTLOG] = "t"; // any non-false value will do it, keep it short } - this.updateLocation(undefined, d); + this.props.updateLocation(undefined, d); console.log('toggleevent'); }, render: function () { - var showEventLog = this.getQuery()[Query.SHOW_EVENTLOG]; + var showEventLog = this.props.query[Query.SHOW_EVENTLOG]; return ( <div> <ToggleComponent @@ -391,7 +389,6 @@ var header_entries = [MainMenu, ViewMenu, OptionMenu /*, ReportsMenu */]; export var Header = React.createClass({ - mixins: [Router], propTypes: { settings: React.PropTypes.object.isRequired, }, @@ -402,7 +399,7 @@ export var Header = React.createClass({ }, handleClick: function (active, e) { e.preventDefault(); - this.updateLocation(active.route); + this.props.updateLocation(active.route); this.setState({active: active}); }, render: function () { @@ -430,7 +427,12 @@ export var Header = React.createClass({ {header} </nav> <div className="menu"> - <this.state.active ref="active" settings={this.props.settings}/> + <this.state.active + ref="active" + settings={this.props.settings} + updateLocation={this.props.updateLocation} + query={this.props.query} + /> </div> </header> ); diff --git a/web/src/js/components/mainview.js b/web/src/js/components/mainview.js index 87c0c4bd..964e82db 100644 --- a/web/src/js/components/mainview.js +++ b/web/src/js/components/mainview.js @@ -5,12 +5,11 @@ import {Query} from "../actions.js"; import {Key} from "../utils.js"; import {StoreView} from "../store/view.js"; import Filt from "../filt/filt.js"; -import { Router, Splitter} from "./common.js" +import {Splitter} from "./common.js" import FlowTable from "./flowtable.js"; import FlowView from "./flowview/index.js"; var MainView = React.createClass({ - mixins: [Router], contextTypes: { flowStore: React.PropTypes.object.isRequired, }, @@ -41,9 +40,9 @@ var MainView = React.createClass({ }, getViewFilt: function () { try { - var filtStr = this.getQuery()[Query.SEARCH]; + var filtStr = this.props.query[Query.SEARCH]; var filt = filtStr ? Filt.parse(filtStr) : () => true; - var highlightStr = this.getQuery()[Query.HIGHLIGHT]; + var highlightStr = this.props.query[Query.HIGHLIGHT]; var highlight = highlightStr ? Filt.parse(highlightStr) : () => false; } catch (e) { console.error("Error when processing filter: " + e); @@ -94,10 +93,10 @@ var MainView = React.createClass({ selectFlow: function (flow) { if (flow) { var tab = this.props.routeParams.detailTab || "request"; - this.updateLocation(`/flows/${flow.id}/${tab}`); + this.props.updateLocation(`/flows/${flow.id}/${tab}`); this.refs.flowTable.scrollIntoView(flow); } else { - this.updateLocation("/flows"); + this.props.updateLocation("/flows"); } }, selectFlowRelative: function (shift) { @@ -225,6 +224,8 @@ var MainView = React.createClass({ key="flowDetails" ref="flowDetails" tab={this.props.routeParams.detailTab} + query={this.props.query} + updateLocation={this.props.updateLocation} flow={selected}/> ]; } else { diff --git a/web/src/js/components/proxyapp.js b/web/src/js/components/proxyapp.js index d17a1522..f47c5bb4 100644 --- a/web/src/js/components/proxyapp.js +++ b/web/src/js/components/proxyapp.js @@ -2,7 +2,7 @@ import React from "react"; import ReactDOM from "react-dom"; import _ from "lodash"; -import {Router, Splitter} from "./common.js" +import {Splitter} from "./common.js" import MainView from "./mainview.js"; import Footer from "./footer.js"; import {Header, MainMenu} from "./header.js"; @@ -21,13 +21,34 @@ var Reports = React.createClass({ var ProxyAppMain = React.createClass({ - mixins: [Router], childContextTypes: { flowStore: React.PropTypes.object.isRequired, eventStore: React.PropTypes.object.isRequired, returnFocus: React.PropTypes.func.isRequired, location: React.PropTypes.object.isRequired, }, + contextTypes: { + router: React.PropTypes.object.isRequired + }, + updateLocation: function (pathname, queryUpdate) { + if (pathname === undefined) { + pathname = this.props.location.pathname; + } + var query = this.props.location.query; + if (queryUpdate !== undefined) { + for (var i in queryUpdate) { + if (queryUpdate.hasOwnProperty(i)) { + query[i] = queryUpdate[i] || undefined; //falsey values shall be removed. + } + } + } + this.context.router.replace({pathname, query}); + }, + getQuery: function () { + // For whatever reason, react-router always returns the same object, which makes comparing + // the current props with nextProps impossible. As a workaround, we just clone the query object. + return _.clone(this.props.location.query); + }, componentDidMount: function () { this.focus(); this.settingsStore.addListener("recalculate", this.onSettingsChange); @@ -97,23 +118,23 @@ var ProxyAppMain = React.createClass({ e.preventDefault(); }, render: function () { + var query = this.getQuery(); var eventlog; if (this.props.location.query[Query.SHOW_EVENTLOG]) { eventlog = [ <Splitter key="splitter" axis="y"/>, - <EventLog key="eventlog"/> + <EventLog key="eventlog" updateLocation={this.updateLocation}/> ]; } else { eventlog = null; } - var children = React.cloneElement( - this.props.children, - { ref: "view", location: this.props.location } - ); return ( <div id="container" tabIndex="0" onKeyDown={this.onKeydown}> - <Header ref="header" settings={this.state.settings}/> - {children} + <Header ref="header" settings={this.state.settings} updateLocation={this.updateLocation} query={query} /> + {React.cloneElement( + this.props.children, + { ref: "view", location: this.props.location , updateLocation: this.updateLocation, query } + )} {eventlog} <Footer settings={this.state.settings}/> </div> @@ -125,12 +146,12 @@ var ProxyAppMain = React.createClass({ import { Route, Router as ReactRouter, hashHistory, Redirect} from "react-router"; export var app = ( -<ReactRouter history={hashHistory}> - <Redirect from="/" to="/flows" /> - <Route path="/" component={ProxyAppMain}> - <Route path="flows" component={MainView}/> - <Route path="flows/:flowId/:detailTab" component={MainView}/> - <Route path="reports" component={Reports}/> - </Route> -</ReactRouter> -);
\ No newline at end of file + <ReactRouter history={hashHistory}> + <Redirect from="/" to="/flows" /> + <Route path="/" component={ProxyAppMain}> + <Route path="flows" component={MainView}/> + <Route path="flows/:flowId/:detailTab" component={MainView}/> + <Route path="reports" component={Reports}/> + </Route> + </ReactRouter> +); |