Merge branch 'main' of https://github.com/gorogoroumaru/mitmproxy into hover-menu

This commit is contained in:
zokutyou2@gmail.com 2021-06-10 22:17:34 +09:00
commit 6824666424
26 changed files with 485 additions and 264 deletions

View File

@ -275,6 +275,8 @@ class ClearAll(RequestHandler):
class ResumeFlows(RequestHandler): class ResumeFlows(RequestHandler):
def post(self): def post(self):
for f in self.view: for f in self.view:
if not f.intercepted:
continue
f.resume() f.resume()
self.view.update([f]) self.view.update([f])

View File

@ -11,6 +11,13 @@ and activate your virtualenv environment before proceeding.**
- Run `yarn test` to run the test suite. - Run `yarn test` to run the test suite.
## Advanced Tools
- `yarn run gulp` supports live-reloading if you install a matching
[browser extension](http://livereload.com/extensions/).
- You can debug application state using the [Redux DevTools](https://github.com/reduxjs/redux-devtools).
## Architecture ## Architecture
There are two components: There are two components:

View File

@ -2,7 +2,6 @@
.user-select (@val) { .user-select (@val) {
-webkit-touch-callout: @val; -webkit-touch-callout: @val;
-webkit-user-select: @val; -webkit-user-select: @val;
-khtml-user-select: @val;
-moz-user-select: @val; -moz-user-select: @val;
-ms-user-select: @val; -ms-user-select: @val;
user-select: @val; user-select: @val;
@ -33,6 +32,7 @@
&.sort-asc, &.sort-desc { &.sort-asc, &.sort-desc {
background-color: lighten(#F2F2F2, 3%); background-color: lighten(#F2F2F2, 3%);
} }
&.sort-asc:after, &.sort-desc:after { &.sort-asc:after, &.sort-desc:after {
font: normal normal normal 14px/1 FontAwesome; font: normal normal normal 14px/1 FontAwesome;
position: absolute; position: absolute;
@ -41,9 +41,11 @@
padding: 2px; padding: 2px;
background-color: fadeout(lighten(#F2F2F2, 3%), 20%); background-color: fadeout(lighten(#F2F2F2, 3%), 20%);
} }
&.sort-asc:after { &.sort-asc:after {
content: "\f0de"; content: "\f0de";
} }
&.sort-desc:after { &.sort-desc:after {
content: "\f0dd"; content: "\f0dd";
} }
@ -53,17 +55,22 @@
tr { tr {
cursor: pointer; cursor: pointer;
background-color: white;
&:nth-child(even) { &:nth-child(even) {
background-color: rgba(0, 0, 0, 0.05); background-color: #f2f2f2;
} }
&.selected { &.selected {
background-color: hsla(209, 52%, 84%, 0.5) !important; background-color: #e0ebf5 !important;
} }
&.highlighted { &.highlighted {
background-color: hsla(48, 100%, 50%, 0.4); background-color: #ffeb99;
} }
&.highlighted:nth-child(even) { &.highlighted:nth-child(even) {
background-color: hsla(48, 100%, 50%, 0.5); background-color: #ffe57f;
} }
} }
@ -80,6 +87,7 @@
color: @interceptorange; color: @interceptorange;
} }
} }
tr.intercepted.has-response { tr.intercepted.has-response {
.col-status, .col-size, .col-time { .col-status, .col-size, .col-time {
color: @interceptorange; color: @interceptorange;
@ -88,6 +96,7 @@
.fa { .fa {
line-height: inherit; line-height: inherit;
&.pull-right { &.pull-right {
margin-left: 0; margin-left: 0;
} }
@ -96,39 +105,89 @@
.col-tls { .col-tls {
width: 10px; width: 10px;
} }
.col-tls-https { .col-tls-https {
background-color: rgba(0, 185, 0, 0.5); background-color: rgba(0, 185, 0, 0.5);
} }
.col-icon { .col-icon {
width: 32px; width: 32px;
} }
.col-path { .col-path {
.fa-repeat { .fa-repeat {
color: green; color: green;
} }
.fa-pause { .fa-pause {
color: @interceptorange; color: @interceptorange;
} }
.fa-exclamation, .fa-times { .fa-exclamation, .fa-times {
color: darkred; color: darkred;
} }
} }
.col-method { .col-method {
width: 60px; width: 60px;
} }
.col-status { .col-status {
width: 50px; width: 50px;
} }
.col-size { .col-size {
width: 70px; width: 70px;
} }
.col-time { .col-time {
width: 50px; width: 50px;
} }
.col-timestamp { .col-timestamp {
width: auto; width: auto;
} }
td.col-time, td.col-size { td.col-time, td.col-size {
text-align: right; text-align: right;
} }
.col-quickactions {
width: 0;
direction: rtl;
overflow: hidden;
background-color: inherit;
font-size: 20px;
}
tr:hover .col-quickactions {
overflow: visible;
}
.col-quickactions > div {
height: 32px;
background-color: inherit;
display: inline-flex;
align-items: center;
div {
margin-right: 2px;
height: 32px;
width: 32px;
border-radius: 16px;
&:hover {
background-color: rgba(0, 0, 0, 5%);
}
}
}
.col-quickactions .fa-ellipsis-h {
transform: translate(-3px, 3px);
}
.col-quickactions .fa-play {
transform: translate(-1px, 2px);
}
} }

View File

@ -3,7 +3,7 @@
@import (reference) "../../node_modules/bootstrap/less/mixins/labels.less"; @import (reference) "../../node_modules/bootstrap/less/mixins/labels.less";
@import (reference) "../../node_modules/bootstrap/less/labels.less"; @import (reference) "../../node_modules/bootstrap/less/labels.less";
@menu-height: 85px; @menu-height: 95px;
header { header {
padding-top: 6px; padding-top: 6px;
@ -20,17 +20,16 @@ header {
} }
@menu-legend-height: 16px; @menu-legend-height: 16px;
@menu-group-hmargin: 3px; @menu-group-hmargin: 5px;
.menu-group { .menu-group {
margin: 0 @menu-group-hmargin; margin: 0 @menu-group-hmargin 0 (@menu-group-hmargin + 1px);
display: inline-block; display: inline-block;
height: @menu-height; height: @menu-height;
vertical-align: top;
} }
.menu-content { .menu-content {
height: @menu-height - @menu-legend-height; height: @menu-height - @menu-legend-height;
text-align: center; display: flow-root;
> .btn { > .btn {
height: @menu-height - @menu-legend-height; height: @menu-height - @menu-legend-height;
@ -45,6 +44,16 @@ header {
margin: 0 auto 5px; margin: 0 auto 5px;
} }
} }
> .btn.btn-sm {
height: (@menu-height - @menu-legend-height)/3;
padding: 0 5px;
i {
display: inline-block;
font-size: 14px;
margin: 0;
}
}
} }
.menu-entry { .menu-entry {
@ -65,6 +74,7 @@ header {
} }
.menu-legend { .menu-legend {
color: #777;
height: @menu-legend-height; height: @menu-legend-height;
text-align: center; text-align: center;
font-size: 12px; font-size: 12px;
@ -73,7 +83,7 @@ header {
.menu-group + .menu-group:before { .menu-group + .menu-group:before {
@space: 10px; @space: 10px;
margin-left: -@menu-group-hmargin; margin-left: -(@menu-group-hmargin + 1px);
content: " "; content: " ";
border-left: solid 1px lighten(grey, 40%); border-left: solid 1px lighten(grey, 40%);
margin-top: @space; margin-top: @space;
@ -82,19 +92,20 @@ header {
} }
@menu-main-gutter-width: 5px; @menu-main-gutter-width: 5px;
.menu-main { .main-menu {
.make-row(@menu-main-gutter-width); display: flex;
padding: 2px 5px; .menu-group {
width: 50%;
}
.btn-sm {
margin-top: 6px;
}
} }
.filter-input { .filter-input {
.make-sm-column(5, @menu-main-gutter-width); margin: 4px 0;
padding: 2.5px;
@media (max-width: @screen-xs-max) { @media (max-width: @screen-xs-max) {
padding: 2px 2.5px;
> .form-control, > .input-group-addon, > .input-group-btn > .btn { > .form-control, > .input-group-addon, > .input-group-btn > .btn {
height: 23.5px; height: 23.5px;
padding: 1px 5px; padding: 1px 5px;

View File

@ -4,12 +4,12 @@ exports[`ShowFullContentButton Component should connect to state 1`] = `null`;
exports[`ShowFullContentButton Component should render correctly 1`] = ` exports[`ShowFullContentButton Component should render correctly 1`] = `
<div> <div>
<div <button
className="view-all-content-btn btn-xs btn btn-default" className="view-all-content-btn btn-xs btn btn-default"
onClick={[Function]} onClick={[Function]}
> >
Show full content Show full content
</div> </button>
<span <span
className="pull-right" className="pull-right"
> >

View File

@ -1,116 +1,135 @@
import React from 'react' import React from 'react'
import renderer from 'react-test-renderer' import renderer from 'react-test-renderer'
import * as Columns from '../../../components/FlowTable/FlowColumns' import {
import { TFlow } from '../../ducks/tutils' IconColumn,
MethodColumn,
PathColumn,
QuickActionsColumn,
SizeColumn,
StatusColumn,
TimeColumn,
TimeStampColumn,
TLSColumn
} from '../../../components/FlowTable/FlowColumns'
import {TFlow, TStore} from '../../ducks/tutils'
import {Provider} from 'react-redux'
describe('FlowColumns Components', () => { describe('Flowcolumns Components', () => {
let tflow = TFlow() let tflow = TFlow()
it('should render TLSColumn', () => { it('should render TLSColumn', () => {
let tlsColumn = renderer.create(<Columns.TLSColumn flow={tflow}/>), let tlsColumn = renderer.create(<TLSColumn flow={tflow}/>),
tree = tlsColumn.toJSON() tree = tlsColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
}) })
it('should render IconColumn', () => { it('should render IconColumn', () => {
let iconColumn = renderer.create(<Columns.IconColumn flow={tflow}/>), let iconColumn = renderer.create(<IconColumn flow={tflow}/>),
tree = iconColumn.toJSON() tree = iconColumn.toJSON()
// plain // plain
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
// not modified // not modified
tflow.response.status_code = 304 tflow.response.status_code = 304
iconColumn = renderer.create(<Columns.IconColumn flow={tflow}/>) iconColumn = renderer.create(<IconColumn flow={tflow}/>)
tree = iconColumn.toJSON() tree = iconColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
// redirect // redirect
tflow.response.status_code = 302 tflow.response.status_code = 302
iconColumn = renderer.create(<Columns.IconColumn flow={tflow}/>) iconColumn = renderer.create(<IconColumn flow={tflow}/>)
tree = iconColumn.toJSON() tree = iconColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
// image // image
let imageFlow = TFlow() let imageFlow = TFlow()
imageFlow.response.headers = [['Content-Type', 'image/jpeg']] imageFlow.response.headers = [['Content-Type', 'image/jpeg']]
iconColumn = renderer.create(<Columns.IconColumn flow={imageFlow}/>) iconColumn = renderer.create(<IconColumn flow={imageFlow}/>)
tree = iconColumn.toJSON() tree = iconColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
// javascript // javascript
let jsFlow = TFlow() let jsFlow = TFlow()
jsFlow.response.headers = [['Content-Type', 'application/x-javascript']] jsFlow.response.headers = [['Content-Type', 'application/x-javascript']]
iconColumn = renderer.create(<Columns.IconColumn flow={jsFlow}/>) iconColumn = renderer.create(<IconColumn flow={jsFlow}/>)
tree = iconColumn.toJSON() tree = iconColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
// css // css
let cssFlow = TFlow() let cssFlow = TFlow()
cssFlow.response.headers = [['Content-Type', 'text/css']] cssFlow.response.headers = [['Content-Type', 'text/css']]
iconColumn = renderer.create(<Columns.IconColumn flow={cssFlow}/>) iconColumn = renderer.create(<IconColumn flow={cssFlow}/>)
tree = iconColumn.toJSON() tree = iconColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
// html // html
let htmlFlow = TFlow() let htmlFlow = TFlow()
htmlFlow.response.headers = [['Content-Type', 'text/html']] htmlFlow.response.headers = [['Content-Type', 'text/html']]
iconColumn = renderer.create(<Columns.IconColumn flow={htmlFlow}/>) iconColumn = renderer.create(<IconColumn flow={htmlFlow}/>)
tree = iconColumn.toJSON() tree = iconColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
// default // default
let fooFlow = TFlow() let fooFlow = TFlow()
fooFlow.response.headers = [['Content-Type', 'foo']] fooFlow.response.headers = [['Content-Type', 'foo']]
iconColumn = renderer.create(<Columns.IconColumn flow={fooFlow}/>) iconColumn = renderer.create(<IconColumn flow={fooFlow}/>)
tree = iconColumn.toJSON() tree = iconColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
// no response // no response
tflow.response = null tflow.response = null
iconColumn = renderer.create(<Columns.IconColumn flow={tflow}/>) iconColumn = renderer.create(<IconColumn flow={tflow}/>)
tree = iconColumn.toJSON() tree = iconColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
}) })
it('should render pathColumn', () => { it('should render pathColumn', () => {
let pathColumn = renderer.create(<Columns.PathColumn flow={tflow}/>), let pathColumn = renderer.create(<PathColumn flow={tflow}/>),
tree = pathColumn.toJSON() tree = pathColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
tflow.error.msg = 'Connection killed.' tflow.error.msg = 'Connection killed.'
tflow.intercepted = true tflow.intercepted = true
pathColumn = renderer.create(<Columns.PathColumn flow={tflow}/>) pathColumn = renderer.create(<PathColumn flow={tflow}/>)
tree = pathColumn.toJSON() tree = pathColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
}) })
it('should render MethodColumn', () => { it('should render MethodColumn', () => {
let methodColumn =renderer.create(<Columns.MethodColumn flow={tflow}/>), let methodColumn = renderer.create(<MethodColumn flow={tflow}/>),
tree = methodColumn.toJSON() tree = methodColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
}) })
it('should render StatusColumn', () => { it('should render StatusColumn', () => {
let statusColumn = renderer.create(<Columns.StatusColumn flow={tflow}/>), let statusColumn = renderer.create(<StatusColumn flow={tflow}/>),
tree = statusColumn.toJSON() tree = statusColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
}) })
it('should render SizeColumn', () => { it('should render SizeColumn', () => {
tflow = TFlow() tflow = TFlow()
let sizeColumn = renderer.create(<Columns.SizeColumn flow={tflow}/>), let sizeColumn = renderer.create(<SizeColumn flow={tflow}/>),
tree = sizeColumn.toJSON() tree = sizeColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
}) })
it('should render TimeColumn', () => { it('should render TimeColumn', () => {
let timeColumn = renderer.create(<Columns.TimeColumn flow={tflow}/>), let timeColumn = renderer.create(<TimeColumn flow={tflow}/>),
tree = timeColumn.toJSON() tree = timeColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
tflow.response = null tflow.response = null
timeColumn = renderer.create(<Columns.TimeColumn flow={tflow}/>), timeColumn = renderer.create(<TimeColumn flow={tflow}/>)
tree = timeColumn.toJSON() tree = timeColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
}) })
it('should render TimeStampColumn', () => { it('should render TimeStampColumn', () => {
let timeStampColumn = renderer.create(<Columns.TimeStampColumn flow={tflow}/>), let timeStampColumn = renderer.create(<TimeStampColumn flow={tflow}/>),
tree = timeStampColumn.toJSON() tree = timeStampColumn.toJSON()
tflow.request.timestamp_start = expect(tree).toMatchSnapshot()
})
it('should render QuickActionsColumn', () => {
let store = TStore(),
provider = renderer.create(
<Provider store={store}>
<QuickActionsColumn flow={tflow}/>
</Provider>),
tree = provider.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
}) })
}) })

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FlowColumns Components should render IconColumn 1`] = ` exports[`Flowcolumns Components should render IconColumn 1`] = `
<td <td
className="col-icon" className="col-icon"
> >
@ -10,7 +10,7 @@ exports[`FlowColumns Components should render IconColumn 1`] = `
</td> </td>
`; `;
exports[`FlowColumns Components should render IconColumn 2`] = ` exports[`Flowcolumns Components should render IconColumn 2`] = `
<td <td
className="col-icon" className="col-icon"
> >
@ -20,7 +20,7 @@ exports[`FlowColumns Components should render IconColumn 2`] = `
</td> </td>
`; `;
exports[`FlowColumns Components should render IconColumn 3`] = ` exports[`Flowcolumns Components should render IconColumn 3`] = `
<td <td
className="col-icon" className="col-icon"
> >
@ -30,7 +30,7 @@ exports[`FlowColumns Components should render IconColumn 3`] = `
</td> </td>
`; `;
exports[`FlowColumns Components should render IconColumn 4`] = ` exports[`Flowcolumns Components should render IconColumn 4`] = `
<td <td
className="col-icon" className="col-icon"
> >
@ -40,7 +40,7 @@ exports[`FlowColumns Components should render IconColumn 4`] = `
</td> </td>
`; `;
exports[`FlowColumns Components should render IconColumn 5`] = ` exports[`Flowcolumns Components should render IconColumn 5`] = `
<td <td
className="col-icon" className="col-icon"
> >
@ -50,7 +50,7 @@ exports[`FlowColumns Components should render IconColumn 5`] = `
</td> </td>
`; `;
exports[`FlowColumns Components should render IconColumn 6`] = ` exports[`Flowcolumns Components should render IconColumn 6`] = `
<td <td
className="col-icon" className="col-icon"
> >
@ -60,7 +60,7 @@ exports[`FlowColumns Components should render IconColumn 6`] = `
</td> </td>
`; `;
exports[`FlowColumns Components should render IconColumn 7`] = ` exports[`Flowcolumns Components should render IconColumn 7`] = `
<td <td
className="col-icon" className="col-icon"
> >
@ -70,7 +70,7 @@ exports[`FlowColumns Components should render IconColumn 7`] = `
</td> </td>
`; `;
exports[`FlowColumns Components should render IconColumn 8`] = ` exports[`Flowcolumns Components should render IconColumn 8`] = `
<td <td
className="col-icon" className="col-icon"
> >
@ -80,7 +80,7 @@ exports[`FlowColumns Components should render IconColumn 8`] = `
</td> </td>
`; `;
exports[`FlowColumns Components should render IconColumn 9`] = ` exports[`Flowcolumns Components should render IconColumn 9`] = `
<td <td
className="col-icon" className="col-icon"
> >
@ -90,7 +90,7 @@ exports[`FlowColumns Components should render IconColumn 9`] = `
</td> </td>
`; `;
exports[`FlowColumns Components should render MethodColumn 1`] = ` exports[`Flowcolumns Components should render MethodColumn 1`] = `
<td <td
className="col-method" className="col-method"
> >
@ -98,7 +98,23 @@ exports[`FlowColumns Components should render MethodColumn 1`] = `
</td> </td>
`; `;
exports[`FlowColumns Components should render SizeColumn 1`] = ` exports[`Flowcolumns Components should render QuickActionsColumn 1`] = `
<td
className="col-quickactions"
>
<div>
<div
className="quickaction"
>
<i
className="fa fa-fw fa-ellipsis-h"
/>
</div>
</div>
</td>
`;
exports[`Flowcolumns Components should render SizeColumn 1`] = `
<td <td
className="col-size" className="col-size"
> >
@ -106,7 +122,7 @@ exports[`FlowColumns Components should render SizeColumn 1`] = `
</td> </td>
`; `;
exports[`FlowColumns Components should render StatusColumn 1`] = ` exports[`Flowcolumns Components should render StatusColumn 1`] = `
<td <td
className="col-status" className="col-status"
style={ style={
@ -117,13 +133,13 @@ exports[`FlowColumns Components should render StatusColumn 1`] = `
/> />
`; `;
exports[`FlowColumns Components should render TLSColumn 1`] = ` exports[`Flowcolumns Components should render TLSColumn 1`] = `
<td <td
className="col-tls col-tls-http" className="col-tls col-tls-http"
/> />
`; `;
exports[`FlowColumns Components should render TimeColumn 1`] = ` exports[`Flowcolumns Components should render TimeColumn 1`] = `
<td <td
className="col-time" className="col-time"
> >
@ -131,7 +147,7 @@ exports[`FlowColumns Components should render TimeColumn 1`] = `
</td> </td>
`; `;
exports[`FlowColumns Components should render TimeColumn 2`] = ` exports[`Flowcolumns Components should render TimeColumn 2`] = `
<td <td
className="col-time" className="col-time"
> >
@ -139,7 +155,7 @@ exports[`FlowColumns Components should render TimeColumn 2`] = `
</td> </td>
`; `;
exports[`FlowColumns Components should render TimeStampColumn 1`] = ` exports[`Flowcolumns Components should render TimeStampColumn 1`] = `
<td <td
className="col-start" className="col-start"
> >
@ -147,7 +163,7 @@ exports[`FlowColumns Components should render TimeStampColumn 1`] = `
</td> </td>
`; `;
exports[`FlowColumns Components should render pathColumn 1`] = ` exports[`Flowcolumns Components should render pathColumn 1`] = `
<td <td
className="col-path" className="col-path"
> >
@ -158,7 +174,7 @@ exports[`FlowColumns Components should render pathColumn 1`] = `
</td> </td>
`; `;
exports[`FlowColumns Components should render pathColumn 2`] = ` exports[`Flowcolumns Components should render pathColumn 2`] = `
<td <td
className="col-path" className="col-path"
> >

View File

@ -48,6 +48,19 @@ exports[`FlowRow Component should render correctly 1`] = `
> >
3s 3s
</td> </td>
<td
className="col-quickactions"
>
<div>
<div
className="quickaction"
>
<i
className="fa fa-fw fa-ellipsis-h"
/>
</div>
</div>
</td>
<div <div
className="dropdown pull-left btn btn-default" className="dropdown pull-left btn btn-default"
> >
@ -103,20 +116,6 @@ exports[`FlowRow Component should render correctly 1`] = `
</a> </a>
</li> </li>
<li>
<a
href="#"
onClick={[Function]}
>
<i
className="fa fa-fw fa-plus"
/>
 Intercept all requests from
192.168.0.1
</a>
</li>
</ul> </ul>
</div> </div>
</tr> </tr>

View File

@ -44,6 +44,12 @@ exports[`FlowTableHead Component should connect to state 1`] = `
> >
Time Time
</th> </th>
<th
className="col-quickactions"
onClick={[Function]}
>
</th>
</tr> </tr>
`; `;
@ -97,5 +103,11 @@ exports[`FlowTableHead Component should render correctly 1`] = `
> >
Time Time
</th> </th>
<th
className="col-quickactions"
onClick={[Function]}
>
</th>
</tr> </tr>
`; `;

View File

@ -16,7 +16,7 @@ describe('FilterInput Component', () => {
let filterInput = TestUtil.renderIntoDocument( let filterInput = TestUtil.renderIntoDocument(
<FilterInput type='foo' color='red' placeholder='bar' value='' onChange={jest.fn()}/>) <FilterInput type='foo' color='red' placeholder='bar' value='' onChange={jest.fn()}/>)
it('should handle componentWillReceiveProps', () => { it('should handle componentWillReceiveProps', () => {
filterInput.componentWillReceiveProps({value: 'foo'}) filterInput.UNSAFE_componentWillReceiveProps({value: 'foo'})
expect(filterInput.state.value).toEqual('foo') expect(filterInput.state.value).toEqual('foo')
}) })

View File

@ -1,14 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FlowMenu Component should connect to state 1`] = ` exports[`FlowMenu Component should connect to state 1`] = `
<div> <div
className="flow-menu"
>
<div <div
className="menu-group" className="menu-group"
> >
<div <div
className="menu-content" className="menu-content"
> >
<div <button
className="btn btn-default" className="btn btn-default"
onClick={[Function]} onClick={[Function]}
title="[r]eplay flow" title="[r]eplay flow"
@ -17,8 +19,8 @@ exports[`FlowMenu Component should connect to state 1`] = `
className="fa fa-fw fa-repeat text-primary" className="fa fa-fw fa-repeat text-primary"
/> />
Replay Replay
</div> </button>
<div <button
className="btn btn-default" className="btn btn-default"
onClick={[Function]} onClick={[Function]}
title="[D]uplicate flow" title="[D]uplicate flow"
@ -27,8 +29,8 @@ exports[`FlowMenu Component should connect to state 1`] = `
className="fa fa-fw fa-copy text-info" className="fa fa-fw fa-copy text-info"
/> />
Duplicate Duplicate
</div> </button>
<div <button
className="btn btn-default" className="btn btn-default"
disabled={true} disabled={true}
title="revert changes to flow [V]" title="revert changes to flow [V]"
@ -37,8 +39,8 @@ exports[`FlowMenu Component should connect to state 1`] = `
className="fa fa-fw fa-history text-warning" className="fa fa-fw fa-history text-warning"
/> />
Revert Revert
</div> </button>
<div <button
className="btn btn-default" className="btn btn-default"
onClick={[Function]} onClick={[Function]}
title="[d]elete flow" title="[d]elete flow"
@ -47,7 +49,7 @@ exports[`FlowMenu Component should connect to state 1`] = `
className="fa fa-fw fa-trash text-danger" className="fa fa-fw fa-trash text-danger"
/> />
Delete Delete
</div> </button>
</div> </div>
<div <div
className="menu-legend" className="menu-legend"
@ -61,7 +63,7 @@ exports[`FlowMenu Component should connect to state 1`] = `
<div <div
className="menu-content" className="menu-content"
> >
<div <button
className="btn btn-default" className="btn btn-default"
onClick={[Function]} onClick={[Function]}
title="download" title="download"
@ -70,7 +72,7 @@ exports[`FlowMenu Component should connect to state 1`] = `
className="fa fa-fw fa-download" className="fa fa-fw fa-download"
/> />
Download Download
</div> </button>
</div> </div>
<div <div
className="menu-legend" className="menu-legend"
@ -84,7 +86,7 @@ exports[`FlowMenu Component should connect to state 1`] = `
<div <div
className="menu-content" className="menu-content"
> >
<div <button
className="btn btn-default" className="btn btn-default"
disabled={true} disabled={true}
title="[a]ccept intercepted flow" title="[a]ccept intercepted flow"
@ -93,8 +95,8 @@ exports[`FlowMenu Component should connect to state 1`] = `
className="fa fa-fw fa-play text-success" className="fa fa-fw fa-play text-success"
/> />
Resume Resume
</div> </button>
<div <button
className="btn btn-default" className="btn btn-default"
disabled={true} disabled={true}
title="kill intercepted flow [x]" title="kill intercepted flow [x]"
@ -103,7 +105,7 @@ exports[`FlowMenu Component should connect to state 1`] = `
className="fa fa-fw fa-times text-danger" className="fa fa-fw fa-times text-danger"
/> />
Abort Abort
</div> </button>
</div> </div>
<div <div
className="menu-legend" className="menu-legend"
@ -115,14 +117,16 @@ exports[`FlowMenu Component should connect to state 1`] = `
`; `;
exports[`FlowMenu Component should render correctly with flow 1`] = ` exports[`FlowMenu Component should render correctly with flow 1`] = `
<div> <div
className="flow-menu"
>
<div <div
className="menu-group" className="menu-group"
> >
<div <div
className="menu-content" className="menu-content"
> >
<div <button
className="btn btn-default" className="btn btn-default"
onClick={[Function]} onClick={[Function]}
title="[r]eplay flow" title="[r]eplay flow"
@ -131,8 +135,8 @@ exports[`FlowMenu Component should render correctly with flow 1`] = `
className="fa fa-fw fa-repeat text-primary" className="fa fa-fw fa-repeat text-primary"
/> />
Replay Replay
</div> </button>
<div <button
className="btn btn-default" className="btn btn-default"
onClick={[Function]} onClick={[Function]}
title="[D]uplicate flow" title="[D]uplicate flow"
@ -141,8 +145,8 @@ exports[`FlowMenu Component should render correctly with flow 1`] = `
className="fa fa-fw fa-copy text-info" className="fa fa-fw fa-copy text-info"
/> />
Duplicate Duplicate
</div> </button>
<div <button
className="btn btn-default" className="btn btn-default"
disabled={false} disabled={false}
onClick={[Function]} onClick={[Function]}
@ -152,8 +156,8 @@ exports[`FlowMenu Component should render correctly with flow 1`] = `
className="fa fa-fw fa-history text-warning" className="fa fa-fw fa-history text-warning"
/> />
Revert Revert
</div> </button>
<div <button
className="btn btn-default" className="btn btn-default"
onClick={[Function]} onClick={[Function]}
title="[d]elete flow" title="[d]elete flow"
@ -162,7 +166,7 @@ exports[`FlowMenu Component should render correctly with flow 1`] = `
className="fa fa-fw fa-trash text-danger" className="fa fa-fw fa-trash text-danger"
/> />
Delete Delete
</div> </button>
</div> </div>
<div <div
className="menu-legend" className="menu-legend"
@ -176,7 +180,7 @@ exports[`FlowMenu Component should render correctly with flow 1`] = `
<div <div
className="menu-content" className="menu-content"
> >
<div <button
className="btn btn-default" className="btn btn-default"
onClick={[Function]} onClick={[Function]}
title="download" title="download"
@ -185,7 +189,7 @@ exports[`FlowMenu Component should render correctly with flow 1`] = `
className="fa fa-fw fa-download" className="fa fa-fw fa-download"
/> />
Download Download
</div> </button>
</div> </div>
<div <div
className="menu-legend" className="menu-legend"
@ -199,7 +203,7 @@ exports[`FlowMenu Component should render correctly with flow 1`] = `
<div <div
className="menu-content" className="menu-content"
> >
<div <button
className="btn btn-default" className="btn btn-default"
disabled={false} disabled={false}
onClick={[Function]} onClick={[Function]}
@ -209,8 +213,8 @@ exports[`FlowMenu Component should render correctly with flow 1`] = `
className="fa fa-fw fa-play text-success" className="fa fa-fw fa-play text-success"
/> />
Resume Resume
</div> </button>
<div <button
className="btn btn-default" className="btn btn-default"
disabled={false} disabled={false}
onClick={[Function]} onClick={[Function]}
@ -220,7 +224,7 @@ exports[`FlowMenu Component should render correctly with flow 1`] = `
className="fa fa-fw fa-times text-danger" className="fa fa-fw fa-times text-danger"
/> />
Abort Abort
</div> </button>
</div> </div>
<div <div
className="menu-legend" className="menu-legend"

View File

@ -2,7 +2,13 @@
exports[`MainMenu Component should render and connect to state 1`] = ` exports[`MainMenu Component should render and connect to state 1`] = `
<div <div
className="menu-main" className="main-menu"
>
<div
className="menu-group"
>
<div
className="menu-content"
> >
<div <div
className="filter-input input-group" className="filter-input input-group"
@ -56,6 +62,19 @@ exports[`MainMenu Component should render and connect to state 1`] = `
value="~a bar" value="~a bar"
/> />
</div> </div>
</div>
<div
className="menu-legend"
>
Find
</div>
</div>
<div
className="menu-group"
>
<div
className="menu-content"
>
<div <div
className="filter-input input-group" className="filter-input input-group"
> >
@ -82,5 +101,22 @@ exports[`MainMenu Component should render and connect to state 1`] = `
value="" value=""
/> />
</div> </div>
<button
className="btn-sm btn btn-default"
onClick={[Function]}
title="[a]ccept all"
>
<i
className="fa fa-fw fa-forward text-success"
/>
Resume All
</button>
</div>
<div
className="menu-legend"
>
Intercept
</div>
</div>
</div> </div>
`; `;

View File

@ -8,7 +8,7 @@ exports[`OptionMenu Component should render correctly 1`] = `
<div <div
className="menu-content" className="menu-content"
> >
<div <button
className="btn btn-default" className="btn btn-default"
onClick={[Function]} onClick={[Function]}
title="Open Options" title="Open Options"
@ -20,7 +20,7 @@ exports[`OptionMenu Component should render correctly 1`] = `
<sup> <sup>
alpha alpha
</sup> </sup>
</div> </button>
</div> </div>
<div <div
className="menu-legend" className="menu-legend"

View File

@ -56,6 +56,12 @@ exports[`FlowTable Component should render correctly 1`] = `
> >
Time Time
</th> </th>
<th
className="col-quickactions"
onClick={[Function]}
>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@ -24,7 +24,7 @@ describe('Dropdown Component', () => {
it('should handle open/close action', () => { it('should handle open/close action', () => {
document.body.addEventListener('click', ()=>{}) document.body.addEventListener('click', ()=>{})
let tree = dropup.toJSON(), let tree = dropup.toJSON(),
e = { preventDefault: jest.fn() } e = { preventDefault: jest.fn(), stopPropagation: jest.fn() }
tree.children[0].props.onClick(e) tree.children[0].props.onClick(e)
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()

View File

@ -1,18 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Button Component should be able to be disabled 1`] = ` exports[`Button Component should be able to be disabled 1`] = `
<div <button
className="classname btn btn-default" className="classname btn btn-default"
disabled="true" disabled="true"
> >
<a> <a>
foo foo
</a> </a>
</div> </button>
`; `;
exports[`Button Component should render correctly 1`] = ` exports[`Button Component should render correctly 1`] = `
<div <button
className="classname btn btn-default" className="classname btn btn-default"
onClick={[Function]} onClick={[Function]}
title="title" title="title"
@ -23,5 +23,5 @@ exports[`Button Component should render correctly 1`] = `
<a> <a>
foo foo
</a> </a>
</div> </button>
`; `;

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import {render} from 'react-dom' import {render} from 'react-dom'
import { applyMiddleware, createStore } from 'redux' import {applyMiddleware, compose, createStore} from 'redux'
import {Provider} from 'react-redux' import {Provider} from 'react-redux'
import thunk from 'redux-thunk' import thunk from 'redux-thunk'
@ -15,14 +15,16 @@ import { logger } from 'redux-logger'
const middlewares = [thunk]; const middlewares = [thunk];
// logger must be last
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
middlewares.push(logger); middlewares.push(logger);
} }
// logger must be last
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore( const store = createStore(
rootReducer, rootReducer,
applyMiddleware(...middlewares) composeEnhancers(applyMiddleware(...middlewares))
) )
useUrlState(store) useUrlState(store)

View File

@ -12,7 +12,7 @@ ViewSelector.propTypes = {
} }
export function ViewSelector ({contentViews, activeView, setContentView}){ export function ViewSelector ({contentViews, activeView, setContentView}){
let inner = <span> <b>View:</b> {activeView.toLowerCase()} <span className="caret"></span> </span> let inner = <span> <b>View:</b> {activeView.toLowerCase()} <span className="caret"/> </span>
return ( return (
<Dropdown dropup className="pull-left" btnClass="btn btn-default btn-xs" text={inner}> <Dropdown dropup className="pull-left" btnClass="btn btn-default btn-xs" text={inner}>
@ -28,7 +28,7 @@ export function ViewSelector ({contentViews, activeView, setContentView}){
export default connect ( export default connect (
state => ({ state => ({
contentViews: state.settings.contentViews, contentViews: state.settings.contentViews || [],
activeView: state.ui.flow.contentView, activeView: state.ui.flow.contentView,
}), { }), {
setContentView, setContentView,

View File

@ -1,13 +1,15 @@
import React, { Component } from 'react' import React from 'react'
import {useDispatch} from 'react-redux'
import classnames from 'classnames' import classnames from 'classnames'
import {RequestUtils, ResponseUtils} from '../../flow/utils.js' import {RequestUtils, ResponseUtils} from '../../flow/utils.js'
import {formatSize, formatTimeDelta, formatTimeStamp} from '../../utils.js' import {formatSize, formatTimeDelta, formatTimeStamp} from '../../utils.js'
import * as flowActions from "../../ducks/flows";
export const defaultColumnNames = ["tls", "icon", "path", "method", "status", "size", "time"] export const defaultColumnNames = ["tls", "icon", "path", "method", "status", "size", "time"]
export function TLSColumn({flow}) { export function TLSColumn({flow}) {
return ( return (
<td className={classnames('col-tls', flow.request.scheme === 'https' ? 'col-tls-https' : 'col-tls-http')}></td> <td className={classnames('col-tls', flow.request.scheme === 'https' ? 'col-tls-https' : 'col-tls-http')}/>
) )
} }
@ -17,7 +19,7 @@ TLSColumn.headerName = ''
export function IconColumn({flow}) { export function IconColumn({flow}) {
return ( return (
<td className="col-icon"> <td className="col-icon">
<div className={classnames('resource-icon', IconColumn.getIcon(flow))}></div> <div className={classnames('resource-icon', IconColumn.getIcon(flow))}/>
</td> </td>
) )
} }
@ -60,18 +62,18 @@ export function PathColumn({ flow }) {
let err; let err;
if (flow.error) { if (flow.error) {
if (flow.error.msg === "Connection killed.") { if (flow.error.msg === "Connection killed.") {
err = <i className="fa fa-fw fa-times pull-right"></i> err = <i className="fa fa-fw fa-times pull-right"/>
} else { } else {
err = <i className="fa fa-fw fa-exclamation pull-right"></i> err = <i className="fa fa-fw fa-exclamation pull-right"/>
} }
} }
return ( return (
<td className="col-path"> <td className="col-path">
{flow.request.is_replay && ( {flow.request.is_replay && (
<i className="fa fa-fw fa-repeat pull-right"></i> <i className="fa fa-fw fa-repeat pull-right"/>
)} )}
{flow.intercepted && ( {flow.intercepted && (
<i className="fa fa-fw fa-pause pull-right"></i> <i className="fa fa-fw fa-pause pull-right"/>
)} )}
{err} {err}
{RequestUtils.pretty_url(flow.request)} {RequestUtils.pretty_url(flow.request)}
@ -96,17 +98,13 @@ export function StatusColumn({ flow }) {
if (flow.response && 100 <= flow.response.status_code && flow.response.status_code < 200) { if (flow.response && 100 <= flow.response.status_code && flow.response.status_code < 200) {
color = 'green' color = 'green'
} } else if (flow.response && 200 <= flow.response.status_code && flow.response.status_code < 300) {
else if (flow.response && 200 <= flow.response.status_code && flow.response.status_code < 300) {
color = 'darkgreen' color = 'darkgreen'
} } else if (flow.response && 300 <= flow.response.status_code && flow.response.status_code < 400) {
else if (flow.response && 300 <= flow.response.status_code && flow.response.status_code < 400) {
color = 'lightblue' color = 'lightblue'
} } else if (flow.response && 400 <= flow.response.status_code && flow.response.status_code < 500) {
else if (flow.response && 400 <= flow.response.status_code && flow.response.status_code < 500) {
color = 'lightred' color = 'lightred'
} } else if (flow.response && 500 <= flow.response.status_code && flow.response.status_code < 600) {
else if (flow.response && 500 <= flow.response.status_code && flow.response.status_code < 600) {
color = 'lightred' color = 'lightred'
} }
@ -165,7 +163,33 @@ export function TimeStampColumn({ flow }) {
TimeStampColumn.headerClass = 'col-timestamp' TimeStampColumn.headerClass = 'col-timestamp'
TimeStampColumn.headerName = 'TimeStamp' TimeStampColumn.headerName = 'TimeStamp'
export default [ export function QuickActionsColumn({flow}) {
const dispatch = useDispatch()
function resume(e) {
dispatch(flowActions.resume(flow))
e.preventDefault()
e.stopPropagation()
}
return (
<td className="col-quickactions">
<div>
<div className="quickaction"><i className="fa fa-fw fa-ellipsis-h"/></div>
{flow.intercepted
? <div className="quickaction" onClick={resume}><i className="fa fa-fw fa-play text-success"/></div>
: null}
</div>
</td>
)
}
QuickActionsColumn.headerClass = 'col-quickactions'
QuickActionsColumn.headerName = ''
export const columns = {};
for (let col of [
TLSColumn, TLSColumn,
IconColumn, IconColumn,
PathColumn, PathColumn,
@ -174,4 +198,7 @@ export default [
TimeStampColumn, TimeStampColumn,
SizeColumn, SizeColumn,
TimeColumn, TimeColumn,
] QuickActionsColumn,
]) {
columns[col.name.replace(/Column$/, "").toLowerCase()] = col;
}

View File

@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import classnames from 'classnames' import classnames from 'classnames'
import columns, {defaultColumnNames} from './FlowColumns' import {columns, defaultColumnNames} from './FlowColumns'
import {setSort} from '../../ducks/flows' import {setSort} from '../../ducks/flows'
@ -14,16 +14,10 @@ FlowTableHead.propTypes = {
} }
export function getDisplayColumns(displayColumnNames) { export function getDisplayColumns(displayColumnNames) {
let displayColumns = []
if (typeof displayColumnNames == "undefined") { if (typeof displayColumnNames == "undefined") {
return columns return Object.values(columns)
} }
for (const column of columns) { return displayColumnNames.map(x => columns[x]).concat([columns.quickactions]);
if (displayColumnNames.includes(column.name.slice(0,-6).toLowerCase())) {
displayColumns.push(column)
}
}
return displayColumns
} }
export function FlowTableHead({sortColumn, sortDesc, setSort, displayColumnNames}) { export function FlowTableHead({sortColumn, sortDesc, setSort, displayColumnNames}) {

View File

@ -13,7 +13,7 @@ function Footer({ settings }) {
stickyauth, stickycookie, stream_large_bodies, listen_host, listen_port, version, server} = settings; stickyauth, stickycookie, stream_large_bodies, listen_host, listen_port, version, server} = settings;
return ( return (
<footer> <footer>
{mode && mode != "regular" && ( {mode && mode !== "regular" && (
<span className="label label-success">{mode} mode</span> <span className="label label-success">{mode} mode</span>
)} )}
{intercept && ( {intercept && (
@ -25,8 +25,8 @@ function Footer({ settings }) {
{no_upstream_cert && ( {no_upstream_cert && (
<span className="label label-success">no-upstream-cert</span> <span className="label label-success">no-upstream-cert</span>
)} )}
{rawtcp && ( {!rawtcp && (
<span className="label label-success">raw-tcp</span> <span className="label label-success">no-raw-tcp</span>
)} )}
{!http2 && ( {!http2 && (
<span className="label label-success">no-http2</span> <span className="label label-success">no-http2</span>

View File

@ -22,7 +22,7 @@ export function FlowMenu({ flow, resumeFlow, killFlow, replayFlow, duplicateFlow
if (!flow) if (!flow)
return <div/> return <div/>
return ( return (
<div> <div className="flow-menu">
<HideInStatic> <HideInStatic>
<div className="menu-group"> <div className="menu-group">
<div className="menu-content"> <div className="menu-content">
@ -72,8 +72,6 @@ export function FlowMenu({ flow, resumeFlow, killFlow, replayFlow, duplicateFlow
<div className="menu-legend">Interception</div> <div className="menu-legend">Interception</div>
</div> </div>
</HideInStatic> </HideInStatic>
</div> </div>
) )
} }

View File

@ -1,24 +1,37 @@
import React, { Component } from "react" import React from "react"
import PropTypes from 'prop-types'
import {connect} from "react-redux" import {connect} from "react-redux"
import FilterInput from "./FilterInput" import FilterInput from "./FilterInput"
import {update as updateSettings} from "../../ducks/settings" import {update as updateSettings} from "../../ducks/settings"
import * as flowsActions from "../../ducks/flows"
import {setFilter, setHighlight} from "../../ducks/flows" import {setFilter, setHighlight} from "../../ducks/flows"
import Button from "../common/Button"
MainMenu.title = "Start" MainMenu.title = "Start"
export default function MainMenu() { export default function MainMenu() {
return ( return (
<div className="menu-main"> <div className="main-menu">
<div className="menu-group">
<div className="menu-content">
<FlowFilterInput/> <FlowFilterInput/>
<HighlightInput/> <HighlightInput/>
</div>
<div className="menu-legend">Find</div>
</div>
<div className="menu-group">
<div className="menu-content">
<InterceptInput/> <InterceptInput/>
<ResumeAll/>
</div>
<div className="menu-legend">Intercept</div>
</div>
</div> </div>
) )
} }
export function setIntercept(intercept) { export function setIntercept(intercept) {
updateSettings({ intercept }) return updateSettings({intercept})
} }
const InterceptInput = connect( const InterceptInput = connect(
@ -50,3 +63,17 @@ const HighlightInput = connect(
}), }),
{onChange: setHighlight} {onChange: setHighlight}
)(FilterInput); )(FilterInput);
export function ResumeAll({resumeAll}) {
return (
<Button className="btn-sm" title="[a]ccept all"
icon="fa-forward text-success" onClick={() => resumeAll()}>
Resume All
</Button>
)
}
ResumeAll = connect(
null,
{resumeAll: flowsActions.resumeAll}
)(ResumeAll)

View File

@ -4,19 +4,19 @@ import classnames from "classnames"
Button.propTypes = { Button.propTypes = {
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
children: PropTypes.node.isRequired, children: PropTypes.node,
icon: PropTypes.string, icon: PropTypes.string,
title: PropTypes.string, title: PropTypes.string,
} }
export default function Button({ onClick, children, icon, disabled, className, title }) { export default function Button({ onClick, children, icon, disabled, className, title }) {
return ( return (
<div className={classnames(className, 'btn btn-default')} <button className={classnames(className, 'btn btn-default')}
onClick={disabled ? undefined : onClick} onClick={disabled ? undefined : onClick}
disabled={disabled} disabled={disabled}
title={title}> title={title}>
{icon && (<i className={"fa fa-fw " + icon}/> )} {icon && (<i className={"fa fa-fw " + icon}/> )}
{children} {children}
</div> </button>
) )
} }

View File

@ -33,6 +33,7 @@ export default class Dropdown extends Component {
if (this.state.open) { if (this.state.open) {
return return
} }
e.stopPropagation();
this.setState({open: !this.state.open}) this.setState({open: !this.state.open})
document.addEventListener('click', this.close) document.addEventListener('click', this.close)
} }

View File

@ -2544,6 +2544,7 @@ dedent@^0.7.0:
deep-diff@^0.3.5: deep-diff@^0.3.5:
version "0.3.8" version "0.3.8"
resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84" resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84"
integrity sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ=
deep-extend@^0.6.0: deep-extend@^0.6.0:
version "0.6.0" version "0.6.0"