[web] various fixes

This commit is contained in:
Maximilian Hils
2016-12-11 22:52:17 +01:00
parent d1c7b203f0
commit d854e08653
20 changed files with 693 additions and 427 deletions

View File

@@ -327,6 +327,9 @@ class View(collections.Sequence):
def resume(self, f):
self.update(f)
def kill(self, f):
self.update(f)
class Focus:
"""

View File

@@ -169,6 +169,8 @@ class Flow(stateobject.StateObject):
self.reply.take()
self.reply.kill(force=True)
self.reply.commit()
self.live = False
master.addons("kill", self)
def intercept(self, master):
"""
@@ -190,4 +192,4 @@ class Flow(stateobject.StateObject):
self.intercepted = False
self.reply.ack()
self.reply.commit()
master.addons("intercept", self)
master.addons("resume", self)

View File

@@ -31,7 +31,8 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
"intercepted": flow.intercepted,
"client_conn": flow.client_conn.get_state(),
"server_conn": flow.server_conn.get_state(),
"type": flow.type
"type": flow.type,
"modified": flow.modified(),
}
if flow.error:
f["error"] = flow.error.get_state()
@@ -222,17 +223,30 @@ class ClearAll(RequestHandler):
self.master.events.clear()
class AcceptFlows(RequestHandler):
class ResumeFlows(RequestHandler):
def post(self):
for f in self.view:
f.resume(self.master)
class AcceptFlow(RequestHandler):
class KillFlows(RequestHandler):
def post(self):
for f in self.view:
if f.killable:
f.kill(self.master)
class ResumeFlow(RequestHandler):
def post(self, flow_id):
self.flow.resume(self.master)
class KillFlow(RequestHandler):
def post(self, flow_id):
if self.flow.killable:
self.flow.kill(self.master)
class FlowHandler(RequestHandler):
def delete(self, flow_id):
if self.flow.killable:
@@ -410,9 +424,11 @@ class Application(tornado.web.Application):
(r"/events", Events),
(r"/flows", Flows),
(r"/flows/dump", DumpFlows),
(r"/flows/accept", AcceptFlows),
(r"/flows/resume", ResumeFlows),
(r"/flows/kill", KillFlows),
(r"/flows/(?P<flow_id>[0-9a-f\-]+)", FlowHandler),
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/accept", AcceptFlow),
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/resume", ResumeFlow),
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/kill", KillFlow),
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/duplicate", DuplicateFlow),
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/replay", ReplayFlow),
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/revert", RevertFlow),

View File

@@ -68,7 +68,7 @@ class WebMaster(master.Master):
app.ClientConnection.broadcast(
resource="flows",
cmd="remove",
data=dict(id=flow.id)
data=flow.id
)
def _sig_view_refresh(self, view):

View File

@@ -130,28 +130,98 @@ body,
margin: 1px 0 0px;
}
header {
padding-top: 0.5em;
padding-top: 6px;
background-color: white;
}
header .menu {
padding: 10px;
header menu {
display: block;
margin: 0;
padding: 0;
border-bottom: solid #a6a6a6 1px;
height: 85px;
overflow: visible;
}
.menu-row {
.menu-group {
margin: 0 3px;
display: inline-block;
height: 85px;
vertical-align: top;
}
.menu-content {
height: 69px;
text-align: center;
}
.menu-content > .btn {
height: 69px;
text-align: center;
margin: 0 1px;
padding: 12px 5px;
border: none;
border-radius: 0;
}
.menu-content > .btn i {
font-size: 20px;
display: block;
margin: 0 auto 5px;
}
.menu-entry {
text-align: left;
height: 23px;
line-height: 1;
padding: 0.5rem 1rem;
}
.menu-entry label {
font-size: 1.2rem;
font-weight: normal;
margin: 0;
}
.menu-entry input[type=checkbox] {
margin: 0 2px;
vertical-align: middle;
}
.menu-legend {
height: 16px;
text-align: center;
font-size: 12px;
padding: 0 5px;
}
.menu-group + .menu-group:before {
margin-left: -3px;
content: " ";
border-left: solid 1px #e6e6e6;
margin-top: 10px;
height: 65px;
position: absolute;
}
.menu-main {
margin-left: -2px;
margin-right: -3px;
padding: 2px 5px;
}
.filter-input {
position: relative;
min-height: 1px;
padding-left: 2.5px;
padding-right: 2.5px;
margin-bottom: 5px;
padding: 2.5px;
}
@media (min-width: 768px) {
.filter-input {
float: left;
width: 25%;
width: 41.66666667%;
}
}
@media (max-width: 767px) {
.filter-input {
padding: 2px 2.5px;
}
.filter-input > .form-control,
.filter-input > .input-group-addon,
.filter-input > .input-group-btn > .btn {
height: 23.5px;
padding: 1px 5px;
font-size: 12px;
line-height: 1.5;
}
}
.filter-input .popover {
@@ -258,6 +328,10 @@ header .menu {
.flow-table .col-path .fa-pause {
color: #ff8000;
}
.flow-table .col-path .fa-exclamation,
.flow-table .col-path .fa-times {
color: darkred;
}
.flow-table .col-method {
width: 60px;
}

File diff suppressed because it is too large Load Diff

View File

@@ -80,17 +80,30 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
self.view.add(f)
self.events.data = events
def test_accept(self):
def test_resume(self):
for f in self.view:
f.reply.handle()
f.intercept(self.master)
assert self.fetch(
"/flows/42/accept", method="POST").code == 200
"/flows/42/resume", method="POST").code == 200
assert sum(f.intercepted for f in self.view) == 1
assert self.fetch("/flows/accept", method="POST").code == 200
assert self.fetch("/flows/resume", method="POST").code == 200
assert all(not f.intercepted for f in self.view)
def test_kill(self):
for f in self.view:
f.backup()
f.reply.handle()
f.intercept(self.master)
assert self.fetch("/flows/42/kill", method="POST").code == 200
assert sum(f.killable for f in self.view) == 1
assert self.fetch("/flows/kill", method="POST").code == 200
assert all(not f.killable for f in self.view)
for f in self.view:
f.revert()
def test_flow_delete(self):
f = self.view.get_by_id("42")
assert f

View File

@@ -109,6 +109,9 @@
.fa-pause {
color: @interceptorange;
}
.fa-exclamation, .fa-times {
color: darkred;
}
}
.col-method {
width: 60px;
@@ -125,4 +128,4 @@
td.col-time, td.col-size {
text-align: right;
}
}
}

View File

@@ -47,6 +47,7 @@ header {
.menu-entry {
text-align: left;
height: (@menu-height - @menu-legend-height)/3;
line-height: 1;
padding: 0.5rem 1rem;

View File

@@ -16,7 +16,9 @@ function ShowFullContentButton ( {setShowFullContent, showFullContent, visibleLi
return (
!showFullContent &&
<div>
<Button className="view-all-content-btn btn-xs" onClick={() => setShowFullContent()} text="Show full content"/>
<Button className="view-all-content-btn btn-xs" onClick={() => setShowFullContent()}>
Show full content
</Button>
<span className="pull-right"> {visibleLines}/{contentLines} are visible &nbsp; </span>
</div>
)

View File

@@ -54,6 +54,15 @@ IconColumn.getIcon = flow => {
}
export function PathColumn({ flow }) {
let err;
if(flow.error){
if (flow.error.msg === "Connection killed"){
err = <i className="fa fa-fw fa-times pull-right"></i>
} else {
err = <i className="fa fa-fw fa-exclamation pull-right"></i>
}
}
return (
<td className="col-path">
{flow.request.is_replay && (
@@ -62,6 +71,7 @@ export function PathColumn({ flow }) {
{flow.intercepted && (
<i className="fa fa-fw fa-pause pull-right"></i>
)}
{err}
{RequestUtils.pretty_url(flow.request)}
</td>
)
@@ -109,7 +119,7 @@ export function TimeColumn({ flow }) {
return (
<td className="col-time">
{flow.response ? (
formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start))
formatTimeDelta(1000 * (flow.response.timestamp_end - flow.server_conn.timestamp_start))
) : (
'...'
)}

View File

@@ -22,7 +22,9 @@ class Header extends Component {
if(selectedFlowId)
entries.push(FlowMenu)
const Active = _.find(entries, (e) => e.title == activeMenu)
// Make sure to have a fallback in case FlowMenu is selected but we don't have any flows
// (e.g. because they are all deleted or not yet received)
const Active = _.find(entries, (e) => e.title == activeMenu) || MainMenu
return (
<header>

View File

@@ -21,23 +21,23 @@ function FileMenu ({clearFlows, loadFlows, saveFlows}) {
<Dropdown className="pull-left" btnClass="special" text="mitmproxy">
<a href="#" onClick={e => FileMenu.onNewClick(e, clearFlows)}>
<i className="fa fa-fw fa-file"></i>
New
&nbsp;New
</a>
<FileChooser
icon="fa-folder-open"
text="Open..."
text="&nbsp;Open..."
onOpenFile={file => loadFlows(file)}
/>
<a href="#" onClick={e =>{ e.preventDefault(); saveFlows();}}>
<i className="fa fa-fw fa-floppy-o"></i>
Save...
&nbsp;Save...
</a>
<Divider/>
<a href="http://mitm.it/" target="_blank">
<i className="fa fa-fw fa-external-link"></i>
Install Certificates...
&nbsp;Install Certificates...
</a>
</Dropdown>
)

View File

@@ -8,21 +8,23 @@ FlowMenu.title = 'Flow'
FlowMenu.propTypes = {
flow: PropTypes.object,
acceptFlow: PropTypes.func.isRequired,
resumeFlow: PropTypes.func.isRequired,
killFlow: PropTypes.func.isRequired,
replayFlow: PropTypes.func.isRequired,
duplicateFlow: PropTypes.func.isRequired,
removeFlow: PropTypes.func.isRequired,
revertFlow: PropTypes.func.isRequired
}
function FlowMenu({ flow, acceptFlow, replayFlow, duplicateFlow, removeFlow, revertFlow }) {
function FlowMenu({ flow, resumeFlow, killFlow, replayFlow, duplicateFlow, removeFlow, revertFlow }) {
if (!flow)
return <div/>
return (
<div>
<div className="menu-group">
<div className="menu-content">
<Button title="[r]eplay flow" icon="fa-repeat text-primary" onClick={() => replayFlow(flow)}>
<Button title="[r]eplay flow" icon="fa-repeat text-primary"
onClick={() => replayFlow(flow)}>
Replay
</Button>
<Button title="[D]uplicate flow" icon="fa-copy text-info"
@@ -33,7 +35,8 @@ function FlowMenu({ flow, acceptFlow, replayFlow, duplicateFlow, removeFlow, rev
icon="fa-history text-warning" onClick={() => revertFlow(flow)}>
Revert
</Button>
<Button title="[d]elete flow" icon="fa-trash text-danger" onClick={() => removeFlow(flow)}>
<Button title="[d]elete flow" icon="fa-trash text-danger"
onClick={() => removeFlow(flow)}>
Delete
</Button>
</div>
@@ -51,17 +54,18 @@ function FlowMenu({ flow, acceptFlow, replayFlow, duplicateFlow, removeFlow, rev
<div className="menu-group">
<div className="menu-content">
<Button disabled={!flow || !flow.intercepted} title="[a]ccept intercepted flow"
icon="fa-play text-success" onClick={() => acceptFlow(flow)}
>
Resume
</Button>
icon="fa-play text-success" onClick={() => resumeFlow(flow)}>
Resume
</Button>
<Button disabled={!flow || !flow.intercepted} title="kill intercepted flow [x]"
icon="fa-times text-danger" onClick={() => killFlow(flow)}>
Abort
</Button>
</div>
<div className="menu-legend">Interception</div>
</div>
</div>
)
}
@@ -71,7 +75,8 @@ export default connect(
flow: state.flows.byId[state.flows.selected[0]],
}),
{
acceptFlow: flowsActions.accept,
resumeFlow: flowsActions.resume,
killFlow: flowsActions.kill,
replayFlow: flowsActions.replay,
duplicateFlow: flowsActions.duplicate,
removeFlow: flowsActions.remove,

View File

@@ -14,7 +14,7 @@ export function MenuToggle({ value, onChange, children }) {
<div className="menu-entry">
<label>
<input type="checkbox"
value={value}
checked={value}
onChange={onChange}/>
{children}
</label>

View File

@@ -11,7 +11,7 @@ Button.propTypes = {
export default function Button({ onClick, children, icon, disabled, className, title }) {
return (
<div className={classnames(className, 'btn btn-default')}
onClick={onClick}
onClick={!disabled && onClick}
disabled={disabled}
title={title}>
{icon && (<i className={"fa fa-fw " + icon}/> )}

View File

@@ -36,8 +36,29 @@ export default function reduce(state = defaultState, action) {
makeFilter(state.filter),
makeSort(state.sort)
)
let selected = state.selected
if(action.type === REMOVE && state.selected.includes(action.data)) {
if(state.selected.length > 1){
selected = selected.filter(x => x !== action.data)
} else {
selected = []
if (action.data in state.viewIndex && state.view.length > 1) {
let currentIndex = state.viewIndex[action.data],
nextSelection
if(currentIndex === state.view.length -1){ // last row
nextSelection = state.view[currentIndex - 1]
} else {
nextSelection = state.view[currentIndex + 1]
}
selected.push(nextSelection.id)
}
}
}
return {
...state,
selected,
...reduceStore(state, storeAction)
}
@@ -48,6 +69,12 @@ export default function reduce(state = defaultState, action) {
...reduceStore(state, storeActions.setFilter(makeFilter(action.filter), makeSort(state.sort)))
}
case SET_HIGHLIGHT:
return {
...state,
highlight: action.highlight
}
case SET_SORT:
return {
...state,
@@ -144,14 +171,23 @@ export function selectRelative(shift) {
}
export function accept(flow) {
return dispatch => fetchApi(`/flows/${flow.id}/accept`, { method: 'POST' })
export function resume(flow) {
return dispatch => fetchApi(`/flows/${flow.id}/resume`, { method: 'POST' })
}
export function acceptAll() {
return dispatch => fetchApi('/flows/accept', { method: 'POST' })
export function resumeAll() {
return dispatch => fetchApi('/flows/resume', { method: 'POST' })
}
export function kill(flow) {
return dispatch => fetchApi(`/flows/${flow.id}/kill`, { method: 'POST' })
}
export function killAll() {
return dispatch => fetchApi('/flows/kill', { method: 'POST' })
}
export function remove(flow) {
return dispatch => fetchApi(`/flows/${flow.id}`, { method: 'DELETE' })
}

View File

@@ -1,4 +1,4 @@
import * as flowsActions from '../flows'
import * as flowsActions from "../flows"
export const SET_ACTIVE_MENU = 'UI_SET_ACTIVE_MENU'
@@ -19,7 +19,7 @@ export default function reducer(state = defaultState, action) {
case flowsActions.SELECT:
// First Select
if (action.flowIds.length && !state.isFlowSelected) {
if (action.flowIds.length > 0 && !state.isFlowSelected) {
return {
...state,
activeMenu: 'Flow',
@@ -28,7 +28,7 @@ export default function reducer(state = defaultState, action) {
}
// Deselect
if (!action.flowIds.length && state.isFlowSelected) {
if (action.flowIds.length === 0 && state.isFlowSelected) {
let activeMenu = state.activeMenu
if (activeMenu == 'Flow') {
activeMenu = 'Start'

View File

@@ -1,6 +1,6 @@
import { Key } from '../../utils'
import { selectTab } from './flow'
import * as flowsActions from '../flows'
import { Key } from "../../utils"
import { selectTab } from "./flow"
import * as flowsActions from "../flows"
export function onKeyDown(e) {
@@ -9,7 +9,7 @@ export function onKeyDown(e) {
return () => {
}
}
var key = e.keyCode
var key = e.keyCode
var shiftKey = e.shiftKey
e.preventDefault()
return (dispatch, getState) => {
@@ -48,9 +48,8 @@ export function onKeyDown(e) {
dispatch(flowsActions.select(null))
break
case Key.LEFT:
{
if(!flow) break
case Key.LEFT: {
if (!flow) break
let tabs = ['request', 'response', 'error'].filter(k => flow[k]).concat(['details']),
currentTab = getState().ui.flow.tab,
nextTab = tabs[(tabs.indexOf(currentTab) - 1 + tabs.length) % tabs.length]
@@ -59,9 +58,8 @@ export function onKeyDown(e) {
}
case Key.TAB:
case Key.RIGHT:
{
if(!flow) break
case Key.RIGHT: {
if (!flow) break
let tabs = ['request', 'response', 'error'].filter(k => flow[k]).concat(['details']),
currentTab = getState().ui.flow.tab,
nextTab = tabs[(tabs.indexOf(currentTab) + 1) % tabs.length]
@@ -69,14 +67,7 @@ export function onKeyDown(e) {
break
}
case Key.C:
if (shiftKey) {
dispatch(flowsActions.clear())
}
break
case Key.D:
{
case Key.D: {
if (!flow) {
return
}
@@ -88,32 +79,46 @@ export function onKeyDown(e) {
break
}
case Key.A:
{
case Key.A: {
if (shiftKey) {
dispatch(flowsActions.acceptAll())
dispatch(flowsActions.resumeAll())
} else if (flow && flow.intercepted) {
dispatch(flowsActions.accept(flow))
dispatch(flowsActions.resume(flow))
}
break
}
case Key.R:
{
case Key.R: {
if (!shiftKey && flow) {
dispatch(flowsActions.replay(flow))
}
break
}
case Key.V:
{
case Key.V: {
if (!shiftKey && flow && flow.modified) {
dispatch(flowsActions.revert(flow))
}
break
}
case Key.X: {
if (shiftKey) {
dispatch(flowsActions.killAll())
} else if (flow && flow.intercepted) {
dispatch(flowsActions.kill(flow))
}
break
}
case Key.Z: {
if (!shiftKey) {
dispatch(flowsActions.clear())
}
break
}
default:
return
}

View File

@@ -85,6 +85,7 @@ export default function reduce(state = defaultState, action) {
if (!(action.id in byId)) {
break
}
byId = {...byId}
delete byId[action.id];
({data: list, dataIndex: listIndex} = removeData(list, listIndex, action.id))