aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml4
-rw-r--r--mitmproxy/console/__init__.py20
-rw-r--r--mitmproxy/controller.py16
-rw-r--r--mitmproxy/flow.py3
-rw-r--r--mitmproxy/proxy/root_context.py1
-rw-r--r--mitmproxy/web/__init__.py9
-rw-r--r--netlib/utils.py14
-rw-r--r--pathod/app.py4
-rw-r--r--pathod/language/__init__.py6
-rw-r--r--pathod/language/base.py16
-rw-r--r--pathod/log.py4
-rw-r--r--pathod/pathoc.py6
-rw-r--r--pathod/pathod.py6
-rw-r--r--pathod/utils.py19
-rw-r--r--test/mitmproxy/test_flow.py5
-rw-r--r--test/netlib/test_utils.py11
-rw-r--r--test/pathod/test_language_base.py2
-rw-r--r--test/pathod/test_utils.py13
-rw-r--r--web/src/js/components/common.js28
-rw-r--r--web/src/js/components/eventlog.js4
-rw-r--r--web/src/js/components/flowview/contentview.js64
-rw-r--r--web/src/js/components/flowview/index.js6
-rw-r--r--web/src/js/components/header.js28
-rw-r--r--web/src/js/components/mainview.js13
-rw-r--r--web/src/js/components/proxyapp.js57
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}/>
&nbsp;
@@ -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>
+);