diff options
author | Maximilian Hils <git@maximilianhils.com> | 2016-02-18 12:29:35 +0100 |
---|---|---|
committer | Maximilian Hils <git@maximilianhils.com> | 2016-02-18 12:29:35 +0100 |
commit | 18b619e164ced91cf0ac8d3fd3c18be1f07df1cc (patch) | |
tree | 071bce256f44f063a2a4b412f43ff2f2b7d3ed5f /web/src/js | |
parent | 3cbacf4e0bae8cd96752303b42f88e176e638824 (diff) | |
download | mitmproxy-18b619e164ced91cf0ac8d3fd3c18be1f07df1cc.tar.gz mitmproxy-18b619e164ced91cf0ac8d3fd3c18be1f07df1cc.tar.bz2 mitmproxy-18b619e164ced91cf0ac8d3fd3c18be1f07df1cc.zip |
move mitmproxy/web to root
Diffstat (limited to 'web/src/js')
27 files changed, 5688 insertions, 0 deletions
diff --git a/web/src/js/actions.js b/web/src/js/actions.js new file mode 100644 index 00000000..2455a52e --- /dev/null +++ b/web/src/js/actions.js @@ -0,0 +1,137 @@ +var $ = require("jquery"); +var _ = require("lodash"); +var AppDispatcher = require("./dispatcher.js").AppDispatcher; + +var ActionTypes = { + // Connection + CONNECTION_OPEN: "connection_open", + CONNECTION_CLOSE: "connection_close", + CONNECTION_ERROR: "connection_error", + + // Stores + SETTINGS_STORE: "settings", + EVENT_STORE: "events", + FLOW_STORE: "flows", +}; + +var StoreCmds = { + ADD: "add", + UPDATE: "update", + REMOVE: "remove", + RESET: "reset" +}; + +var ConnectionActions = { + open: function () { + AppDispatcher.dispatchViewAction({ + type: ActionTypes.CONNECTION_OPEN + }); + }, + close: function () { + AppDispatcher.dispatchViewAction({ + type: ActionTypes.CONNECTION_CLOSE + }); + }, + error: function () { + AppDispatcher.dispatchViewAction({ + type: ActionTypes.CONNECTION_ERROR + }); + } +}; + +var SettingsActions = { + update: function (settings) { + + $.ajax({ + type: "PUT", + url: "/settings", + contentType: 'application/json', + data: JSON.stringify(settings) + }); + + /* + //Facebook Flux: We do an optimistic update on the client already. + AppDispatcher.dispatchViewAction({ + type: ActionTypes.SETTINGS_STORE, + cmd: StoreCmds.UPDATE, + data: settings + }); + */ + } +}; + +var EventLogActions_event_id = 0; +var EventLogActions = { + add_event: function (message) { + AppDispatcher.dispatchViewAction({ + type: ActionTypes.EVENT_STORE, + cmd: StoreCmds.ADD, + data: { + message: message, + level: "web", + id: "viewAction-" + EventLogActions_event_id++ + } + }); + } +}; + +var FlowActions = { + accept: function (flow) { + $.post("/flows/" + flow.id + "/accept"); + }, + accept_all: function(){ + $.post("/flows/accept"); + }, + "delete": function(flow){ + $.ajax({ + type:"DELETE", + url: "/flows/" + flow.id + }); + }, + duplicate: function(flow){ + $.post("/flows/" + flow.id + "/duplicate"); + }, + replay: function(flow){ + $.post("/flows/" + flow.id + "/replay"); + }, + revert: function(flow){ + $.post("/flows/" + flow.id + "/revert"); + }, + update: function (flow, nextProps) { + /* + //Facebook Flux: We do an optimistic update on the client already. + var nextFlow = _.cloneDeep(flow); + _.merge(nextFlow, nextProps); + AppDispatcher.dispatchViewAction({ + type: ActionTypes.FLOW_STORE, + cmd: StoreCmds.UPDATE, + data: nextFlow + }); + */ + $.ajax({ + type: "PUT", + url: "/flows/" + flow.id, + contentType: 'application/json', + data: JSON.stringify(nextProps) + }); + }, + clear: function(){ + $.post("/clear"); + } +}; + +var Query = { + SEARCH: "s", + HIGHLIGHT: "h", + SHOW_EVENTLOG: "e" +}; + +module.exports = { + ActionTypes: ActionTypes, + ConnectionActions: ConnectionActions, + FlowActions: FlowActions, + StoreCmds: StoreCmds, + SettingsActions: SettingsActions, + EventLogActions: EventLogActions, + Query: Query +};
\ No newline at end of file diff --git a/web/src/js/app.js b/web/src/js/app.js new file mode 100644 index 00000000..9fb868b8 --- /dev/null +++ b/web/src/js/app.js @@ -0,0 +1,19 @@ +var React = require("react"); +var ReactRouter = require("react-router"); +var $ = require("jquery"); +var Connection = require("./connection"); +var proxyapp = require("./components/proxyapp.js"); +var EventLogActions = require("./actions.js").EventLogActions; + +$(function () { + window.ws = new Connection("/updates"); + + window.onerror = function (msg) { + EventLogActions.add_event(msg); + }; + + ReactRouter.run(proxyapp.routes, function (Handler, state) { + React.render(<Handler/>, document.body); + }); +}); + diff --git a/web/src/js/components/common.js b/web/src/js/components/common.js new file mode 100644 index 00000000..965ae9a7 --- /dev/null +++ b/web/src/js/components/common.js @@ -0,0 +1,219 @@ +var React = require("react"); +var ReactRouter = require("react-router"); +var _ = require("lodash"); + +// http://blog.vjeux.com/2013/javascript/scroll-position-with-react.html (also contains inverse example) +var AutoScrollMixin = { + componentWillUpdate: function () { + var node = this.getDOMNode(); + this._shouldScrollBottom = ( + node.scrollTop !== 0 && + node.scrollTop + node.clientHeight === node.scrollHeight + ); + }, + componentDidUpdate: function () { + if (this._shouldScrollBottom) { + var node = this.getDOMNode(); + node.scrollTop = node.scrollHeight; + } + }, +}; + + +var StickyHeadMixin = { + adjustHead: function () { + // Abusing CSS transforms to set the element + // referenced as head into some kind of position:sticky. + var head = this.refs.head.getDOMNode(); + head.style.transform = "translate(0," + this.getDOMNode().scrollTop + "px)"; + } +}; + +var SettingsState = { + contextTypes: { + settingsStore: React.PropTypes.object.isRequired + }, + getInitialState: function () { + return { + settings: this.context.settingsStore.dict + }; + }, + componentDidMount: function () { + this.context.settingsStore.addListener("recalculate", this.onSettingsChange); + }, + componentWillUnmount: function () { + this.context.settingsStore.removeListener("recalculate", this.onSettingsChange); + }, + onSettingsChange: function () { + this.setState({ + settings: this.context.settingsStore.dict + }); + }, +}; + + +var ChildFocus = { + contextTypes: { + returnFocus: React.PropTypes.func + }, + returnFocus: function(){ + React.findDOMNode(this).blur(); + window.getSelection().removeAllRanges(); + this.context.returnFocus(); + } +}; + + +var Navigation = _.extend({}, ReactRouter.Navigation, { + setQuery: function (dict) { + var q = this.context.router.getCurrentQuery(); + for (var i in dict) { + if (dict.hasOwnProperty(i)) { + q[i] = dict[i] || undefined; //falsey values shall be removed. + } + } + this.replaceWith(this.context.router.getCurrentPath(), this.context.router.getCurrentParams(), q); + }, + replaceWith: function (routeNameOrPath, params, query) { + if (routeNameOrPath === undefined) { + routeNameOrPath = this.context.router.getCurrentPath(); + } + if (params === undefined) { + params = this.context.router.getCurrentParams(); + } + if (query === undefined) { + query = this.context.router.getCurrentQuery(); + } + + this.context.router.replaceWith(routeNameOrPath, params, query); + } +}); + +// react-router is fairly good at changing its API regularly. +// We keep the old method for now - if it should turn out that their changes are permanent, +// we may remove this mixin and access react-router directly again. +var RouterState = _.extend({}, ReactRouter.State, { + 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.router.getCurrentQuery()); + }, + getParams: function () { + return _.clone(this.context.router.getCurrentParams()); + } +}); + +var Splitter = React.createClass({ + getDefaultProps: function () { + return { + axis: "x" + }; + }, + getInitialState: function () { + return { + applied: false, + startX: false, + startY: false + }; + }, + onMouseDown: function (e) { + this.setState({ + startX: e.pageX, + startY: e.pageY + }); + window.addEventListener("mousemove", this.onMouseMove); + window.addEventListener("mouseup", this.onMouseUp); + // Occasionally, only a dragEnd event is triggered, but no mouseUp. + window.addEventListener("dragend", this.onDragEnd); + }, + onDragEnd: function () { + this.getDOMNode().style.transform = ""; + window.removeEventListener("dragend", this.onDragEnd); + window.removeEventListener("mouseup", this.onMouseUp); + window.removeEventListener("mousemove", this.onMouseMove); + }, + onMouseUp: function (e) { + this.onDragEnd(); + + var node = this.getDOMNode(); + var prev = node.previousElementSibling; + var next = node.nextElementSibling; + + var dX = e.pageX - this.state.startX; + var dY = e.pageY - this.state.startY; + var flexBasis; + if (this.props.axis === "x") { + flexBasis = prev.offsetWidth + dX; + } else { + flexBasis = prev.offsetHeight + dY; + } + + prev.style.flex = "0 0 " + Math.max(0, flexBasis) + "px"; + next.style.flex = "1 1 auto"; + + this.setState({ + applied: true + }); + this.onResize(); + }, + onMouseMove: function (e) { + var dX = 0, dY = 0; + if (this.props.axis === "x") { + dX = e.pageX - this.state.startX; + } else { + dY = e.pageY - this.state.startY; + } + this.getDOMNode().style.transform = "translate(" + dX + "px," + dY + "px)"; + }, + onResize: function () { + // Trigger a global resize event. This notifies components that employ virtual scrolling + // that their viewport may have changed. + window.setTimeout(function () { + window.dispatchEvent(new CustomEvent("resize")); + }, 1); + }, + reset: function (willUnmount) { + if (!this.state.applied) { + return; + } + var node = this.getDOMNode(); + var prev = node.previousElementSibling; + var next = node.nextElementSibling; + + prev.style.flex = ""; + next.style.flex = ""; + + if (!willUnmount) { + this.setState({ + applied: false + }); + } + this.onResize(); + }, + componentWillUnmount: function () { + this.reset(true); + }, + render: function () { + var className = "splitter"; + if (this.props.axis === "x") { + className += " splitter-x"; + } else { + className += " splitter-y"; + } + return ( + <div className={className}> + <div onMouseDown={this.onMouseDown} draggable="true"></div> + </div> + ); + } +}); + +module.exports = { + ChildFocus: ChildFocus, + RouterState: RouterState, + Navigation: Navigation, + StickyHeadMixin: StickyHeadMixin, + AutoScrollMixin: AutoScrollMixin, + Splitter: Splitter, + SettingsState: SettingsState +};
\ No newline at end of file diff --git a/web/src/js/components/editor.js b/web/src/js/components/editor.js new file mode 100644 index 00000000..f2d44566 --- /dev/null +++ b/web/src/js/components/editor.js @@ -0,0 +1,240 @@ +var React = require("react"); +var common = require("./common.js"); +var utils = require("../utils.js"); + +var contentToHtml = function (content) { + return _.escape(content); +}; +var nodeToContent = function (node) { + return node.textContent; +}; + +/* + Basic Editor Functionality + */ +var EditorBase = React.createClass({ + propTypes: { + content: React.PropTypes.string.isRequired, + onDone: React.PropTypes.func.isRequired, + contentToHtml: React.PropTypes.func, + nodeToContent: React.PropTypes.func, // content === nodeToContent( Node<innerHTML=contentToHtml(content)> ) + onStop: React.PropTypes.func, + submitOnEnter: React.PropTypes.bool, + className: React.PropTypes.string, + tag: React.PropTypes.string + }, + getDefaultProps: function () { + return { + contentToHtml: contentToHtml, + nodeToContent: nodeToContent, + submitOnEnter: true, + className: "", + tag: "div" + }; + }, + getInitialState: function () { + return { + editable: false + }; + }, + render: function () { + var className = "inline-input " + this.props.className; + var html = {__html: this.props.contentToHtml(this.props.content)}; + var Tag = this.props.tag; + return <Tag + {...this.props} + tabIndex="0" + className={className} + contentEditable={this.state.editable || undefined } // workaround: use undef instead of false to remove attr + onFocus={this.onFocus} + onMouseDown={this.onMouseDown} + onClick={this.onClick} + onBlur={this._stop} + onKeyDown={this.onKeyDown} + onInput={this.onInput} + onPaste={this.onPaste} + dangerouslySetInnerHTML={html} + />; + }, + onPaste: function (e) { + e.preventDefault(); + var content = e.clipboardData.getData("text/plain"); + document.execCommand("insertHTML", false, content); + }, + onMouseDown: function (e) { + this._mouseDown = true; + window.addEventListener("mouseup", this.onMouseUp); + this.props.onMouseDown && this.props.onMouseDown(e); + }, + onMouseUp: function () { + if (this._mouseDown) { + this._mouseDown = false; + window.removeEventListener("mouseup", this.onMouseUp) + } + }, + onClick: function (e) { + this.onMouseUp(); + this.onFocus(e); + }, + onFocus: function (e) { + console.log("onFocus", this._mouseDown, this._ignore_events, this.state.editable); + if (this._mouseDown || this._ignore_events || this.state.editable) { + return; + } + + //contenteditable in FireFox is more or less broken. + // - we need to blur() and then focus(), otherwise the caret is not shown. + // - blur() + focus() == we need to save the caret position before + // Firefox sometimes just doesn't set a caret position => use caretPositionFromPoint + var sel = window.getSelection(); + var range; + if (sel.rangeCount > 0) { + range = sel.getRangeAt(0); + } else if (document.caretPositionFromPoint && e.clientX && e.clientY) { + var pos = document.caretPositionFromPoint(e.clientX, e.clientY); + range = document.createRange(); + range.setStart(pos.offsetNode, pos.offset); + } else if (document.caretRangeFromPoint && e.clientX && e.clientY) { + range = document.caretRangeFromPoint(e.clientX, e.clientY); + } else { + range = document.createRange(); + range.selectNodeContents(React.findDOMNode(this)); + } + + this._ignore_events = true; + this.setState({editable: true}, function () { + var node = React.findDOMNode(this); + node.blur(); + node.focus(); + this._ignore_events = false; + //sel.removeAllRanges(); + //sel.addRange(range); + + + }); + }, + stop: function () { + // a stop would cause a blur as a side-effect. + // but a blur event must trigger a stop as well. + // to fix this, make stop = blur and do the actual stop in the onBlur handler. + React.findDOMNode(this).blur(); + this.props.onStop && this.props.onStop(); + }, + _stop: function (e) { + if (this._ignore_events) { + return; + } + console.log("_stop", _.extend({}, e)); + window.getSelection().removeAllRanges(); //make sure that selection is cleared on blur + var node = React.findDOMNode(this); + var content = this.props.nodeToContent(node); + this.setState({editable: false}); + this.props.onDone(content); + this.props.onBlur && this.props.onBlur(e); + }, + reset: function () { + React.findDOMNode(this).innerHTML = this.props.contentToHtml(this.props.content); + }, + onKeyDown: function (e) { + e.stopPropagation(); + switch (e.keyCode) { + case utils.Key.ESC: + e.preventDefault(); + this.reset(); + this.stop(); + break; + case utils.Key.ENTER: + if (this.props.submitOnEnter && !e.shiftKey) { + e.preventDefault(); + this.stop(); + } + break; + default: + break; + } + }, + onInput: function () { + var node = React.findDOMNode(this); + var content = this.props.nodeToContent(node); + this.props.onInput && this.props.onInput(content); + } +}); + +/* + Add Validation to EditorBase + */ +var ValidateEditor = React.createClass({ + propTypes: { + content: React.PropTypes.string.isRequired, + onDone: React.PropTypes.func.isRequired, + onInput: React.PropTypes.func, + isValid: React.PropTypes.func, + className: React.PropTypes.string, + }, + getInitialState: function () { + return { + currentContent: this.props.content + }; + }, + componentWillReceiveProps: function () { + this.setState({currentContent: this.props.content}); + }, + onInput: function (content) { + this.setState({currentContent: content}); + this.props.onInput && this.props.onInput(content); + }, + render: function () { + var className = this.props.className || ""; + if (this.props.isValid) { + if (this.props.isValid(this.state.currentContent)) { + className += " has-success"; + } else { + className += " has-warning" + } + } + return <EditorBase + {...this.props} + ref="editor" + className={className} + onDone={this.onDone} + onInput={this.onInput} + />; + }, + onDone: function (content) { + if (this.props.isValid && !this.props.isValid(content)) { + this.refs.editor.reset(); + content = this.props.content; + } + this.props.onDone(content); + } +}); + +/* + Text Editor with mitmweb-specific convenience features + */ +var ValueEditor = React.createClass({ + mixins: [common.ChildFocus], + propTypes: { + content: React.PropTypes.string.isRequired, + onDone: React.PropTypes.func.isRequired, + inline: React.PropTypes.bool, + }, + render: function () { + var tag = this.props.inline ? "span" : "div"; + return <ValidateEditor + {...this.props} + onStop={this.onStop} + tag={tag} + />; + }, + focus: function () { + React.findDOMNode(this).focus(); + }, + onStop: function () { + this.returnFocus(); + } +}); + +module.exports = { + ValueEditor: ValueEditor +};
\ No newline at end of file diff --git a/web/src/js/components/eventlog.js b/web/src/js/components/eventlog.js new file mode 100644 index 00000000..fea7b247 --- /dev/null +++ b/web/src/js/components/eventlog.js @@ -0,0 +1,150 @@ +var React = require("react"); +var common = require("./common.js"); +var Query = require("../actions.js").Query; +var VirtualScrollMixin = require("./virtualscroll.js"); +var views = require("../store/view.js"); +var _ = require("lodash"); + +var LogMessage = React.createClass({ + render: function () { + var entry = this.props.entry; + var indicator; + switch (entry.level) { + case "web": + indicator = <i className="fa fa-fw fa-html5"></i>; + break; + case "debug": + indicator = <i className="fa fa-fw fa-bug"></i>; + break; + default: + indicator = <i className="fa fa-fw fa-info"></i>; + } + return ( + <div> + { indicator } {entry.message} + </div> + ); + }, + shouldComponentUpdate: function () { + return false; // log entries are immutable. + } +}); + +var EventLogContents = React.createClass({ + contextTypes: { + eventStore: React.PropTypes.object.isRequired + }, + mixins: [common.AutoScrollMixin, VirtualScrollMixin], + getInitialState: function () { + var filterFn = function (entry) { + return this.props.filter[entry.level]; + }; + var view = new views.StoreView(this.context.eventStore, filterFn.bind(this)); + view.addListener("add", this.onEventLogChange); + view.addListener("recalculate", this.onEventLogChange); + + return { + view: view + }; + }, + componentWillUnmount: function () { + this.state.view.close(); + }, + filter: function (entry) { + return this.props.filter[entry.level]; + }, + onEventLogChange: function () { + this.forceUpdate(); + }, + componentWillReceiveProps: function (nextProps) { + if (nextProps.filter !== this.props.filter) { + this.props.filter = nextProps.filter; // Dirty: Make sure that view filter sees the update. + this.state.view.recalculate(); + } + }, + getDefaultProps: function () { + return { + rowHeight: 45, + rowHeightMin: 15, + placeholderTagName: "div" + }; + }, + renderRow: function (elem) { + return <LogMessage key={elem.id} entry={elem}/>; + }, + render: function () { + var entries = this.state.view.list; + var rows = this.renderRows(entries); + + return <pre onScroll={this.onScroll}> + { this.getPlaceholderTop(entries.length) } + {rows} + { this.getPlaceholderBottom(entries.length) } + </pre>; + } +}); + +var ToggleFilter = React.createClass({ + toggle: function (e) { + e.preventDefault(); + return this.props.toggleLevel(this.props.name); + }, + render: function () { + var className = "label "; + if (this.props.active) { + className += "label-primary"; + } else { + className += "label-default"; + } + return ( + <a + href="#" + className={className} + onClick={this.toggle}> + {this.props.name} + </a> + ); + } +}); + +var EventLog = React.createClass({ + mixins: [common.Navigation], + getInitialState: function () { + return { + filter: { + "debug": false, + "info": true, + "web": true + } + }; + }, + close: function () { + var d = {}; + d[Query.SHOW_EVENTLOG] = undefined; + this.setQuery(d); + }, + toggleLevel: function (level) { + var filter = _.extend({}, this.state.filter); + filter[level] = !filter[level]; + this.setState({filter: filter}); + }, + render: function () { + return ( + <div className="eventlog"> + <div> + Eventlog + <div className="pull-right"> + <ToggleFilter name="debug" active={this.state.filter.debug} toggleLevel={this.toggleLevel}/> + <ToggleFilter name="info" active={this.state.filter.info} toggleLevel={this.toggleLevel}/> + <ToggleFilter name="web" active={this.state.filter.web} toggleLevel={this.toggleLevel}/> + <i onClick={this.close} className="fa fa-close"></i> + </div> + + </div> + <EventLogContents filter={this.state.filter}/> + </div> + ); + } +}); + +module.exports = EventLog;
\ No newline at end of file diff --git a/web/src/js/components/flowtable-columns.js b/web/src/js/components/flowtable-columns.js new file mode 100644 index 00000000..74d96216 --- /dev/null +++ b/web/src/js/components/flowtable-columns.js @@ -0,0 +1,201 @@ +var React = require("react"); +var RequestUtils = require("../flow/utils.js").RequestUtils; +var ResponseUtils = require("../flow/utils.js").ResponseUtils; +var utils = require("../utils.js"); + +var TLSColumn = React.createClass({ + statics: { + Title: React.createClass({ + render: function(){ + return <th {...this.props} className={"col-tls " + (this.props.className || "") }></th>; + } + }), + sortKeyFun: function(flow){ + return flow.request.scheme; + } + }, + render: function () { + var flow = this.props.flow; + var ssl = (flow.request.scheme === "https"); + var classes; + if (ssl) { + classes = "col-tls col-tls-https"; + } else { + classes = "col-tls col-tls-http"; + } + return <td className={classes}></td>; + } +}); + + +var IconColumn = React.createClass({ + statics: { + Title: React.createClass({ + render: function(){ + return <th {...this.props} className={"col-icon " + (this.props.className || "") }></th>; + } + }) + }, + render: function () { + var flow = this.props.flow; + + var icon; + if (flow.response) { + var contentType = ResponseUtils.getContentType(flow.response); + + //TODO: We should assign a type to the flow somewhere else. + if (flow.response.status_code === 304) { + icon = "resource-icon-not-modified"; + } else if (300 <= flow.response.status_code && flow.response.status_code < 400) { + icon = "resource-icon-redirect"; + } else if (contentType && contentType.indexOf("image") >= 0) { + icon = "resource-icon-image"; + } else if (contentType && contentType.indexOf("javascript") >= 0) { + icon = "resource-icon-js"; + } else if (contentType && contentType.indexOf("css") >= 0) { + icon = "resource-icon-css"; + } else if (contentType && contentType.indexOf("html") >= 0) { + icon = "resource-icon-document"; + } + } + if (!icon) { + icon = "resource-icon-plain"; + } + + + icon += " resource-icon"; + return <td className="col-icon"> + <div className={icon}></div> + </td>; + } +}); + +var PathColumn = React.createClass({ + statics: { + Title: React.createClass({ + render: function(){ + return <th {...this.props} className={"col-path " + (this.props.className || "") }>Path</th>; + } + }), + sortKeyFun: function(flow){ + return RequestUtils.pretty_url(flow.request); + } + }, + render: function () { + var flow = this.props.flow; + return <td className="col-path"> + {flow.request.is_replay ? <i className="fa fa-fw fa-repeat pull-right"></i> : null} + {flow.intercepted ? <i className="fa fa-fw fa-pause pull-right"></i> : null} + { RequestUtils.pretty_url(flow.request) } + </td>; + } +}); + + +var MethodColumn = React.createClass({ + statics: { + Title: React.createClass({ + render: function(){ + return <th {...this.props} className={"col-method " + (this.props.className || "") }>Method</th>; + } + }), + sortKeyFun: function(flow){ + return flow.request.method; + } + }, + render: function () { + var flow = this.props.flow; + return <td className="col-method">{flow.request.method}</td>; + } +}); + + +var StatusColumn = React.createClass({ + statics: { + Title: React.createClass({ + render: function(){ + return <th {...this.props} className={"col-status " + (this.props.className || "") }>Status</th>; + } + }), + sortKeyFun: function(flow){ + return flow.response ? flow.response.status_code : undefined; + } + }, + render: function () { + var flow = this.props.flow; + var status; + if (flow.response) { + status = flow.response.status_code; + } else { + status = null; + } + return <td className="col-status">{status}</td>; + } +}); + + +var SizeColumn = React.createClass({ + statics: { + Title: React.createClass({ + render: function(){ + return <th {...this.props} className={"col-size " + (this.props.className || "") }>Size</th>; + } + }), + sortKeyFun: function(flow){ + var total = flow.request.contentLength; + if (flow.response) { + total += flow.response.contentLength || 0; + } + return total; + } + }, + render: function () { + var flow = this.props.flow; + + var total = flow.request.contentLength; + if (flow.response) { + total += flow.response.contentLength || 0; + } + var size = utils.formatSize(total); + return <td className="col-size">{size}</td>; + } +}); + + +var TimeColumn = React.createClass({ + statics: { + Title: React.createClass({ + render: function(){ + return <th {...this.props} className={"col-time " + (this.props.className || "") }>Time</th>; + } + }), + sortKeyFun: function(flow){ + if(flow.response) { + return flow.response.timestamp_end - flow.request.timestamp_start; + } + } + }, + render: function () { + var flow = this.props.flow; + var time; + if (flow.response) { + time = utils.formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start)); + } else { + time = "..."; + } + return <td className="col-time">{time}</td>; + } +}); + + +var all_columns = [ + TLSColumn, + IconColumn, + PathColumn, + MethodColumn, + StatusColumn, + SizeColumn, + TimeColumn +]; + +module.exports = all_columns; diff --git a/web/src/js/components/flowtable.js b/web/src/js/components/flowtable.js new file mode 100644 index 00000000..609034f6 --- /dev/null +++ b/web/src/js/components/flowtable.js @@ -0,0 +1,187 @@ +var React = require("react"); +var common = require("./common.js"); +var utils = require("../utils.js"); +var _ = require("lodash"); + +var VirtualScrollMixin = require("./virtualscroll.js"); +var flowtable_columns = require("./flowtable-columns.js"); + +var FlowRow = React.createClass({ + render: function () { + var flow = this.props.flow; + var columns = this.props.columns.map(function (Column) { + return <Column key={Column.displayName} flow={flow}/>; + }.bind(this)); + var className = ""; + if (this.props.selected) { + className += " selected"; + } + if (this.props.highlighted) { + className += " highlighted"; + } + if (flow.intercepted) { + className += " intercepted"; + } + if (flow.request) { + className += " has-request"; + } + if (flow.response) { + className += " has-response"; + } + + return ( + <tr className={className} onClick={this.props.selectFlow.bind(null, flow)}> + {columns} + </tr>); + }, + shouldComponentUpdate: function (nextProps) { + return true; + // Further optimization could be done here + // by calling forceUpdate on flow updates, selection changes and column changes. + //return ( + //(this.props.columns.length !== nextProps.columns.length) || + //(this.props.selected !== nextProps.selected) + //); + } +}); + +var FlowTableHead = React.createClass({ + getInitialState: function(){ + return { + sortColumn: undefined, + sortDesc: false + }; + }, + onClick: function(Column){ + var sortDesc = this.state.sortDesc; + var hasSort = Column.sortKeyFun; + if(Column === this.state.sortColumn){ + sortDesc = !sortDesc; + this.setState({ + sortDesc: sortDesc + }); + } else { + this.setState({ + sortColumn: hasSort && Column, + sortDesc: false + }) + } + var sortKeyFun; + if(!sortDesc){ + sortKeyFun = Column.sortKeyFun; + } else { + sortKeyFun = hasSort && function(){ + var k = Column.sortKeyFun.apply(this, arguments); + if(_.isString(k)){ + return utils.reverseString(""+k); + } else { + return -k; + } + } + } + this.props.setSortKeyFun(sortKeyFun); + }, + render: function () { + var columns = this.props.columns.map(function (Column) { + var onClick = this.onClick.bind(this, Column); + var className; + if(this.state.sortColumn === Column) { + if(this.state.sortDesc){ + className = "sort-desc"; + } else { + className = "sort-asc"; + } + } + return <Column.Title + key={Column.displayName} + onClick={onClick} + className={className} />; + }.bind(this)); + return <thead> + <tr>{columns}</tr> + </thead>; + } +}); + + +var ROW_HEIGHT = 32; + +var FlowTable = React.createClass({ + mixins: [common.StickyHeadMixin, common.AutoScrollMixin, VirtualScrollMixin], + contextTypes: { + view: React.PropTypes.object.isRequired + }, + getInitialState: function () { + return { + columns: flowtable_columns + }; + }, + componentWillMount: function () { + this.context.view.addListener("add", this.onChange); + this.context.view.addListener("update", this.onChange); + this.context.view.addListener("remove", this.onChange); + this.context.view.addListener("recalculate", this.onChange); + }, + componentWillUnmount: function(){ + this.context.view.removeListener("add", this.onChange); + this.context.view.removeListener("update", this.onChange); + this.context.view.removeListener("remove", this.onChange); + this.context.view.removeListener("recalculate", this.onChange); + }, + getDefaultProps: function () { + return { + rowHeight: ROW_HEIGHT + }; + }, + onScrollFlowTable: function () { + this.adjustHead(); + this.onScroll(); + }, + onChange: function () { + this.forceUpdate(); + }, + scrollIntoView: function (flow) { + this.scrollRowIntoView( + this.context.view.index(flow), + this.refs.body.getDOMNode().offsetTop + ); + }, + renderRow: function (flow) { + var selected = (flow === this.props.selected); + var highlighted = + ( + this.context.view._highlight && + this.context.view._highlight[flow.id] + ); + + return <FlowRow key={flow.id} + ref={flow.id} + flow={flow} + columns={this.state.columns} + selected={selected} + highlighted={highlighted} + selectFlow={this.props.selectFlow} + />; + }, + render: function () { + var flows = this.context.view.list; + var rows = this.renderRows(flows); + + return ( + <div className="flow-table" onScroll={this.onScrollFlowTable}> + <table> + <FlowTableHead ref="head" + columns={this.state.columns} + setSortKeyFun={this.props.setSortKeyFun}/> + <tbody ref="body"> + { this.getPlaceholderTop(flows.length) } + {rows} + { this.getPlaceholderBottom(flows.length) } + </tbody> + </table> + </div> + ); + } +}); + +module.exports = FlowTable; diff --git a/web/src/js/components/flowview/contentview.js b/web/src/js/components/flowview/contentview.js new file mode 100644 index 00000000..63d22c1c --- /dev/null +++ b/web/src/js/components/flowview/contentview.js @@ -0,0 +1,237 @@ +var React = require("react"); +var _ = require("lodash"); + +var MessageUtils = require("../../flow/utils.js").MessageUtils; +var utils = require("../../utils.js"); + +var image_regex = /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i; +var ViewImage = React.createClass({ + statics: { + matches: function (message) { + return image_regex.test(MessageUtils.getContentType(message)); + } + }, + render: function () { + var url = MessageUtils.getContentURL(this.props.flow, this.props.message); + return <div className="flowview-image"> + <img src={url} alt="preview" className="img-thumbnail"/> + </div>; + } +}); + +var RawMixin = { + getInitialState: function () { + return { + content: undefined, + request: undefined + } + }, + requestContent: function (nextProps) { + if (this.state.request) { + this.state.request.abort(); + } + var request = MessageUtils.getContent(nextProps.flow, nextProps.message); + this.setState({ + content: undefined, + request: request + }); + request.done(function (data) { + this.setState({content: data}); + }.bind(this)).fail(function (jqXHR, textStatus, errorThrown) { + if (textStatus === "abort") { + return; + } + this.setState({content: "AJAX Error: " + textStatus + "\r\n" + errorThrown}); + }.bind(this)).always(function () { + this.setState({request: undefined}); + }.bind(this)); + + }, + componentWillMount: function () { + this.requestContent(this.props); + }, + componentWillReceiveProps: function (nextProps) { + if (nextProps.message !== this.props.message) { + this.requestContent(nextProps); + } + }, + componentWillUnmount: function () { + if (this.state.request) { + this.state.request.abort(); + } + }, + render: function () { + if (!this.state.content) { + return <div className="text-center"> + <i className="fa fa-spinner fa-spin"></i> + </div>; + } + return this.renderContent(); + } +}; + +var ViewRaw = React.createClass({ + mixins: [RawMixin], + statics: { + matches: function (message) { + return true; + } + }, + renderContent: function () { + return <pre>{this.state.content}</pre>; + } +}); + +var json_regex = /^application\/json$/i; +var ViewJSON = React.createClass({ + mixins: [RawMixin], + statics: { + matches: function (message) { + return json_regex.test(MessageUtils.getContentType(message)); + } + }, + renderContent: function () { + var json = this.state.content; + try { + json = JSON.stringify(JSON.parse(json), null, 2); + } catch (e) { + } + return <pre>{json}</pre>; + } +}); + +var ViewAuto = React.createClass({ + statics: { + matches: function () { + return false; // don't match itself + }, + findView: function (message) { + for (var i = 0; i < all.length; i++) { + if (all[i].matches(message)) { + return all[i]; + } + } + return all[all.length - 1]; + } + }, + render: function () { + var View = ViewAuto.findView(this.props.message); + return <View {...this.props}/>; + } +}); + +var all = [ViewAuto, ViewImage, ViewJSON, ViewRaw]; + + +var ContentEmpty = React.createClass({ + render: function () { + var message_name = this.props.flow.request === this.props.message ? "request" : "response"; + return <div className="alert alert-info">No {message_name} content.</div>; + } +}); + +var ContentMissing = React.createClass({ + render: function () { + var message_name = this.props.flow.request === this.props.message ? "Request" : "Response"; + return <div className="alert alert-info">{message_name} content missing.</div>; + } +}); + +var TooLarge = React.createClass({ + statics: { + isTooLarge: function (message) { + var max_mb = ViewImage.matches(message) ? 10 : 0.2; + return message.contentLength > 1024 * 1024 * max_mb; + } + }, + render: function () { + var size = utils.formatSize(this.props.message.contentLength); + return <div className="alert alert-warning"> + <button onClick={this.props.onClick} className="btn btn-xs btn-warning pull-right">Display anyway</button> + {size} content size. + </div>; + } +}); + +var ViewSelector = React.createClass({ + render: function () { + var views = []; + for (var i = 0; i < all.length; i++) { + var view = all[i]; + var className = "btn btn-default"; + if (view === this.props.active) { + className += " active"; + } + var text; + if (view === ViewAuto) { + text = "auto: " + ViewAuto.findView(this.props.message).displayName.toLowerCase().replace("view", ""); + } else { + text = view.displayName.toLowerCase().replace("view", ""); + } + views.push( + <button + key={view.displayName} + onClick={this.props.selectView.bind(null, view)} + className={className}> + {text} + </button> + ); + } + + return <div className="view-selector btn-group btn-group-xs">{views}</div>; + } +}); + +var ContentView = React.createClass({ + getInitialState: function () { + return { + displayLarge: false, + View: ViewAuto + }; + }, + propTypes: { + // It may seem a bit weird at the first glance: + // Every view takes the flow and the message as props, e.g. + // <Auto flow={flow} message={flow.request}/> + flow: React.PropTypes.object.isRequired, + message: React.PropTypes.object.isRequired, + }, + selectView: function (view) { + this.setState({ + View: view + }); + }, + displayLarge: function () { + this.setState({displayLarge: true}); + }, + componentWillReceiveProps: function (nextProps) { + if (nextProps.message !== this.props.message) { + this.setState(this.getInitialState()); + } + }, + render: function () { + var message = this.props.message; + if (message.contentLength === 0) { + return <ContentEmpty {...this.props}/>; + } else if (message.contentLength === null) { + return <ContentMissing {...this.props}/>; + } else if (!this.state.displayLarge && TooLarge.isTooLarge(message)) { + return <TooLarge {...this.props} onClick={this.displayLarge}/>; + } + + var downloadUrl = MessageUtils.getContentURL(this.props.flow, message); + + return <div> + <this.state.View {...this.props} /> + <div className="view-options text-center"> + <ViewSelector selectView={this.selectView} active={this.state.View} message={message}/> + + <a className="btn btn-default btn-xs" href={downloadUrl}> + <i className="fa fa-download"/> + </a> + </div> + </div>; + } +}); + +module.exports = ContentView;
\ No newline at end of file diff --git a/web/src/js/components/flowview/details.js b/web/src/js/components/flowview/details.js new file mode 100644 index 00000000..00e0116c --- /dev/null +++ b/web/src/js/components/flowview/details.js @@ -0,0 +1,181 @@ +var React = require("react"); +var _ = require("lodash"); + +var utils = require("../../utils.js"); + +var TimeStamp = React.createClass({ + render: function () { + + if (!this.props.t) { + //should be return null, but that triggers a React bug. + return <tr></tr>; + } + + var ts = utils.formatTimeStamp(this.props.t); + + var delta; + if (this.props.deltaTo) { + delta = utils.formatTimeDelta(1000 * (this.props.t - this.props.deltaTo)); + delta = <span className="text-muted">{"(" + delta + ")"}</span>; + } else { + delta = null; + } + + return <tr> + <td>{this.props.title + ":"}</td> + <td>{ts} {delta}</td> + </tr>; + } +}); + +var ConnectionInfo = React.createClass({ + + render: function () { + var conn = this.props.conn; + var address = conn.address.address.join(":"); + + var sni = <tr key="sni"></tr>; //should be null, but that triggers a React bug. + if (conn.sni) { + sni = <tr key="sni"> + <td> + <abbr title="TLS Server Name Indication">TLS SNI:</abbr> + </td> + <td>{conn.sni}</td> + </tr>; + } + return ( + <table className="connection-table"> + <tbody> + <tr key="address"> + <td>Address:</td> + <td>{address}</td> + </tr> + {sni} + </tbody> + </table> + ); + } +}); + +var CertificateInfo = React.createClass({ + render: function () { + //TODO: We should fetch human-readable certificate representation + // from the server + var flow = this.props.flow; + var client_conn = flow.client_conn; + var server_conn = flow.server_conn; + + var preStyle = {maxHeight: 100}; + return ( + <div> + {client_conn.cert ? <h4>Client Certificate</h4> : null} + {client_conn.cert ? <pre style={preStyle}>{client_conn.cert}</pre> : null} + + {server_conn.cert ? <h4>Server Certificate</h4> : null} + {server_conn.cert ? <pre style={preStyle}>{server_conn.cert}</pre> : null} + </div> + ); + } +}); + +var Timing = React.createClass({ + render: function () { + var flow = this.props.flow; + var sc = flow.server_conn; + var cc = flow.client_conn; + var req = flow.request; + var resp = flow.response; + + var timestamps = [ + { + title: "Server conn. initiated", + t: sc.timestamp_start, + deltaTo: req.timestamp_start + }, { + title: "Server conn. TCP handshake", + t: sc.timestamp_tcp_setup, + deltaTo: req.timestamp_start + }, { + title: "Server conn. SSL handshake", + t: sc.timestamp_ssl_setup, + deltaTo: req.timestamp_start + }, { + title: "Client conn. established", + t: cc.timestamp_start, + deltaTo: req.timestamp_start + }, { + title: "Client conn. SSL handshake", + t: cc.timestamp_ssl_setup, + deltaTo: req.timestamp_start + }, { + title: "First request byte", + t: req.timestamp_start, + }, { + title: "Request complete", + t: req.timestamp_end, + deltaTo: req.timestamp_start + } + ]; + + if (flow.response) { + timestamps.push( + { + title: "First response byte", + t: resp.timestamp_start, + deltaTo: req.timestamp_start + }, { + title: "Response complete", + t: resp.timestamp_end, + deltaTo: req.timestamp_start + } + ); + } + + //Add unique key for each row. + timestamps.forEach(function (e) { + e.key = e.title; + }); + + timestamps = _.sortBy(timestamps, 't'); + + var rows = timestamps.map(function (e) { + return <TimeStamp {...e}/>; + }); + + return ( + <div> + <h4>Timing</h4> + <table className="timing-table"> + <tbody> + {rows} + </tbody> + </table> + </div> + ); + } +}); + +var Details = React.createClass({ + render: function () { + var flow = this.props.flow; + var client_conn = flow.client_conn; + var server_conn = flow.server_conn; + return ( + <section> + + <h4>Client Connection</h4> + <ConnectionInfo conn={client_conn}/> + + <h4>Server Connection</h4> + <ConnectionInfo conn={server_conn}/> + + <CertificateInfo flow={flow}/> + + <Timing flow={flow}/> + + </section> + ); + } +}); + +module.exports = Details;
\ No newline at end of file diff --git a/web/src/js/components/flowview/index.js b/web/src/js/components/flowview/index.js new file mode 100644 index 00000000..739a46dc --- /dev/null +++ b/web/src/js/components/flowview/index.js @@ -0,0 +1,127 @@ +var React = require("react"); +var _ = require("lodash"); + +var common = require("../common.js"); +var Nav = require("./nav.js"); +var Messages = require("./messages.js"); +var Details = require("./details.js"); +var Prompt = require("../prompt.js"); + + +var allTabs = { + request: Messages.Request, + response: Messages.Response, + error: Messages.Error, + details: Details +}; + +var FlowView = React.createClass({ + mixins: [common.StickyHeadMixin, common.Navigation, common.RouterState], + getInitialState: function () { + return { + prompt: false + }; + }, + getTabs: function (flow) { + var tabs = []; + ["request", "response", "error"].forEach(function (e) { + if (flow[e]) { + tabs.push(e); + } + }); + tabs.push("details"); + return tabs; + }, + nextTab: function (i) { + var tabs = this.getTabs(this.props.flow); + var currentIndex = tabs.indexOf(this.getActive()); + // JS modulo operator doesn't correct negative numbers, make sure that we are positive. + var nextIndex = (currentIndex + i + tabs.length) % tabs.length; + this.selectTab(tabs[nextIndex]); + }, + selectTab: function (panel) { + this.replaceWith( + "flow", + { + flowId: this.getParams().flowId, + detailTab: panel + } + ); + }, + getActive: function(){ + return this.getParams().detailTab; + }, + promptEdit: function () { + var options; + switch(this.getActive()){ + case "request": + options = [ + "method", + "url", + {text:"http version", key:"v"}, + "header" + /*, "content"*/]; + break; + case "response": + options = [ + {text:"http version", key:"v"}, + "code", + "message", + "header" + /*, "content"*/]; + break; + case "details": + return; + default: + throw "Unknown tab for edit: " + this.getActive(); + } + + this.setState({ + prompt: { + done: function (k) { + this.setState({prompt: false}); + if(k){ + this.refs.tab.edit(k); + } + }.bind(this), + options: options + } + }); + }, + render: function () { + var flow = this.props.flow; + var tabs = this.getTabs(flow); + var active = this.getActive(); + + if (!_.contains(tabs, active)) { + if (active === "response" && flow.error) { + active = "error"; + } else if (active === "error" && flow.response) { + active = "response"; + } else { + active = tabs[0]; + } + this.selectTab(active); + } + + var prompt = null; + if (this.state.prompt) { + prompt = <Prompt {...this.state.prompt}/>; + } + + var Tab = allTabs[active]; + return ( + <div className="flow-detail" onScroll={this.adjustHead}> + <Nav ref="head" + flow={flow} + tabs={tabs} + active={active} + selectTab={this.selectTab}/> + <Tab ref="tab" flow={flow}/> + {prompt} + </div> + ); + } +}); + +module.exports = FlowView;
\ No newline at end of file diff --git a/web/src/js/components/flowview/messages.js b/web/src/js/components/flowview/messages.js new file mode 100644 index 00000000..7ac95d85 --- /dev/null +++ b/web/src/js/components/flowview/messages.js @@ -0,0 +1,326 @@ +var React = require("react"); +var _ = require("lodash"); + +var common = require("../common.js"); +var actions = require("../../actions.js"); +var flowutils = require("../../flow/utils.js"); +var utils = require("../../utils.js"); +var ContentView = require("./contentview.js"); +var ValueEditor = require("../editor.js").ValueEditor; + +var Headers = React.createClass({ + propTypes: { + onChange: React.PropTypes.func.isRequired, + message: React.PropTypes.object.isRequired + }, + onChange: function (row, col, val) { + var nextHeaders = _.cloneDeep(this.props.message.headers); + nextHeaders[row][col] = val; + if (!nextHeaders[row][0] && !nextHeaders[row][1]) { + // do not delete last row + if (nextHeaders.length === 1) { + nextHeaders[0][0] = "Name"; + nextHeaders[0][1] = "Value"; + } else { + nextHeaders.splice(row, 1); + // manually move selection target if this has been the last row. + if (row === nextHeaders.length) { + this._nextSel = (row - 1) + "-value"; + } + } + } + this.props.onChange(nextHeaders); + }, + edit: function () { + this.refs["0-key"].focus(); + }, + onTab: function (row, col, e) { + var headers = this.props.message.headers; + if (row === headers.length - 1 && col === 1) { + e.preventDefault(); + + var nextHeaders = _.cloneDeep(this.props.message.headers); + nextHeaders.push(["Name", "Value"]); + this.props.onChange(nextHeaders); + this._nextSel = (row + 1) + "-key"; + } + }, + componentDidUpdate: function () { + if (this._nextSel && this.refs[this._nextSel]) { + this.refs[this._nextSel].focus(); + this._nextSel = undefined; + } + }, + onRemove: function (row, col, e) { + if (col === 1) { + e.preventDefault(); + this.refs[row + "-key"].focus(); + } else if (row > 0) { + e.preventDefault(); + this.refs[(row - 1) + "-value"].focus(); + } + }, + render: function () { + + var rows = this.props.message.headers.map(function (header, i) { + + var kEdit = <HeaderEditor + ref={i + "-key"} + content={header[0]} + onDone={this.onChange.bind(null, i, 0)} + onRemove={this.onRemove.bind(null, i, 0)} + onTab={this.onTab.bind(null, i, 0)}/>; + var vEdit = <HeaderEditor + ref={i + "-value"} + content={header[1]} + onDone={this.onChange.bind(null, i, 1)} + onRemove={this.onRemove.bind(null, i, 1)} + onTab={this.onTab.bind(null, i, 1)}/>; + return ( + <tr key={i}> + <td className="header-name">{kEdit}:</td> + <td className="header-value">{vEdit}</td> + </tr> + ); + }.bind(this)); + return ( + <table className="header-table"> + <tbody> + {rows} + </tbody> + </table> + ); + } +}); + +var HeaderEditor = React.createClass({ + render: function () { + return <ValueEditor ref="input" {...this.props} onKeyDown={this.onKeyDown} inline/>; + }, + focus: function () { + this.getDOMNode().focus(); + }, + onKeyDown: function (e) { + switch (e.keyCode) { + case utils.Key.BACKSPACE: + var s = window.getSelection().getRangeAt(0); + if (s.startOffset === 0 && s.endOffset === 0) { + this.props.onRemove(e); + } + break; + case utils.Key.TAB: + if (!e.shiftKey) { + this.props.onTab(e); + } + break; + } + } +}); + +var RequestLine = React.createClass({ + render: function () { + var flow = this.props.flow; + var url = flowutils.RequestUtils.pretty_url(flow.request); + var httpver = flow.request.http_version; + + return <div className="first-line request-line"> + <ValueEditor + ref="method" + content={flow.request.method} + onDone={this.onMethodChange} + inline/> + + <ValueEditor + ref="url" + content={url} + onDone={this.onUrlChange} + isValid={this.isValidUrl} + inline/> + + <ValueEditor + ref="httpVersion" + content={httpver} + onDone={this.onHttpVersionChange} + isValid={flowutils.isValidHttpVersion} + inline/> + </div> + }, + isValidUrl: function (url) { + var u = flowutils.parseUrl(url); + return !!u.host; + }, + onMethodChange: function (nextMethod) { + actions.FlowActions.update( + this.props.flow, + {request: {method: nextMethod}} + ); + }, + onUrlChange: function (nextUrl) { + var props = flowutils.parseUrl(nextUrl); + props.path = props.path || ""; + actions.FlowActions.update( + this.props.flow, + {request: props} + ); + }, + onHttpVersionChange: function (nextVer) { + var ver = flowutils.parseHttpVersion(nextVer); + actions.FlowActions.update( + this.props.flow, + {request: {http_version: ver}} + ); + } +}); + +var ResponseLine = React.createClass({ + render: function () { + var flow = this.props.flow; + var httpver = flow.response.http_version; + return <div className="first-line response-line"> + <ValueEditor + ref="httpVersion" + content={httpver} + onDone={this.onHttpVersionChange} + isValid={flowutils.isValidHttpVersion} + inline/> + + <ValueEditor + ref="code" + content={flow.response.status_code + ""} + onDone={this.onCodeChange} + isValid={this.isValidCode} + inline/> + + <ValueEditor + ref="msg" + content={flow.response.msg} + onDone={this.onMsgChange} + inline/> + </div>; + }, + isValidCode: function (code) { + return /^\d+$/.test(code); + }, + onHttpVersionChange: function (nextVer) { + var ver = flowutils.parseHttpVersion(nextVer); + actions.FlowActions.update( + this.props.flow, + {response: {http_version: ver}} + ); + }, + onMsgChange: function (nextMsg) { + actions.FlowActions.update( + this.props.flow, + {response: {msg: nextMsg}} + ); + }, + onCodeChange: function (nextCode) { + nextCode = parseInt(nextCode); + actions.FlowActions.update( + this.props.flow, + {response: {code: nextCode}} + ); + } +}); + +var Request = React.createClass({ + render: function () { + var flow = this.props.flow; + return ( + <section className="request"> + <RequestLine ref="requestLine" flow={flow}/> + {/*<ResponseLine flow={flow}/>*/} + <Headers ref="headers" message={flow.request} onChange={this.onHeaderChange}/> + <hr/> + <ContentView flow={flow} message={flow.request}/> + </section> + ); + }, + edit: function (k) { + switch (k) { + case "m": + this.refs.requestLine.refs.method.focus(); + break; + case "u": + this.refs.requestLine.refs.url.focus(); + break; + case "v": + this.refs.requestLine.refs.httpVersion.focus(); + break; + case "h": + this.refs.headers.edit(); + break; + default: + throw "Unimplemented: " + k; + } + }, + onHeaderChange: function (nextHeaders) { + actions.FlowActions.update(this.props.flow, { + request: { + headers: nextHeaders + } + }); + } +}); + +var Response = React.createClass({ + render: function () { + var flow = this.props.flow; + return ( + <section className="response"> + {/*<RequestLine flow={flow}/>*/} + <ResponseLine ref="responseLine" flow={flow}/> + <Headers ref="headers" message={flow.response} onChange={this.onHeaderChange}/> + <hr/> + <ContentView flow={flow} message={flow.response}/> + </section> + ); + }, + edit: function (k) { + switch (k) { + case "c": + this.refs.responseLine.refs.status_code.focus(); + break; + case "m": + this.refs.responseLine.refs.msg.focus(); + break; + case "v": + this.refs.responseLine.refs.httpVersion.focus(); + break; + case "h": + this.refs.headers.edit(); + break; + default: + throw "Unimplemented: " + k; + } + }, + onHeaderChange: function (nextHeaders) { + actions.FlowActions.update(this.props.flow, { + response: { + headers: nextHeaders + } + }); + } +}); + +var Error = React.createClass({ + render: function () { + var flow = this.props.flow; + return ( + <section> + <div className="alert alert-warning"> + {flow.error.msg} + <div> + <small>{ utils.formatTimeStamp(flow.error.timestamp) }</small> + </div> + </div> + </section> + ); + } +}); + +module.exports = { + Request: Request, + Response: Response, + Error: Error +}; diff --git a/web/src/js/components/flowview/nav.js b/web/src/js/components/flowview/nav.js new file mode 100644 index 00000000..46eda707 --- /dev/null +++ b/web/src/js/components/flowview/nav.js @@ -0,0 +1,61 @@ +var React = require("react"); + +var actions = require("../../actions.js"); + +var NavAction = React.createClass({ + onClick: function (e) { + e.preventDefault(); + this.props.onClick(); + }, + render: function () { + return ( + <a title={this.props.title} + href="#" + className="nav-action" + onClick={this.onClick}> + <i className={"fa fa-fw " + this.props.icon}></i> + </a> + ); + } +}); + +var Nav = React.createClass({ + render: function () { + var flow = this.props.flow; + + var tabs = this.props.tabs.map(function (e) { + var str = e.charAt(0).toUpperCase() + e.slice(1); + var className = this.props.active === e ? "active" : ""; + var onClick = function (event) { + this.props.selectTab(e); + event.preventDefault(); + }.bind(this); + return <a key={e} + href="#" + className={className} + onClick={onClick}>{str}</a>; + }.bind(this)); + + var acceptButton = null; + if(flow.intercepted){ + acceptButton = <NavAction title="[a]ccept intercepted flow" icon="fa-play" onClick={actions.FlowActions.accept.bind(null, flow)} />; + } + var revertButton = null; + if(flow.modified){ + revertButton = <NavAction title="revert changes to flow [V]" icon="fa-history" onClick={actions.FlowActions.revert.bind(null, flow)} />; + } + + return ( + <nav ref="head" className="nav-tabs nav-tabs-sm"> + {tabs} + <NavAction title="[d]elete flow" icon="fa-trash" onClick={actions.FlowActions.delete.bind(null, flow)} /> + <NavAction title="[D]uplicate flow" icon="fa-copy" onClick={actions.FlowActions.duplicate.bind(null, flow)} /> + <NavAction disabled title="[r]eplay flow" icon="fa-repeat" onClick={actions.FlowActions.replay.bind(null, flow)} /> + {acceptButton} + {revertButton} + </nav> + ); + } +}); + +module.exports = Nav;
\ No newline at end of file diff --git a/web/src/js/components/footer.js b/web/src/js/components/footer.js new file mode 100644 index 00000000..229d691b --- /dev/null +++ b/web/src/js/components/footer.js @@ -0,0 +1,19 @@ +var React = require("react"); +var common = require("./common.js"); + +var Footer = React.createClass({ + mixins: [common.SettingsState], + render: function () { + var mode = this.state.settings.mode; + var intercept = this.state.settings.intercept; + return ( + <footer> + {mode && mode != "regular" ? <span className="label label-success">{mode} mode</span> : null} + + {intercept ? <span className="label label-success">Intercept: {intercept}</span> : null} + </footer> + ); + } +}); + +module.exports = Footer;
\ No newline at end of file diff --git a/web/src/js/components/header.js b/web/src/js/components/header.js new file mode 100644 index 00000000..998a41df --- /dev/null +++ b/web/src/js/components/header.js @@ -0,0 +1,399 @@ +var React = require("react"); +var $ = require("jquery"); + +var Filt = require("../filt/filt.js"); +var utils = require("../utils.js"); +var common = require("./common.js"); +var actions = require("../actions.js"); +var Query = require("../actions.js").Query; + +var FilterDocs = React.createClass({ + statics: { + xhr: false, + doc: false + }, + componentWillMount: function () { + if (!FilterDocs.doc) { + FilterDocs.xhr = $.getJSON("/filter-help").done(function (doc) { + FilterDocs.doc = doc; + FilterDocs.xhr = false; + }); + } + if (FilterDocs.xhr) { + FilterDocs.xhr.done(function () { + this.forceUpdate(); + }.bind(this)); + } + }, + render: function () { + if (!FilterDocs.doc) { + return <i className="fa fa-spinner fa-spin"></i>; + } else { + var commands = FilterDocs.doc.commands.map(function (c) { + return <tr key={c[1]}> + <td>{c[0].replace(" ", '\u00a0')}</td> + <td>{c[1]}</td> + </tr>; + }); + commands.push(<tr key="docs-link"> + <td colSpan="2"> + <a href="https://mitmproxy.org/doc/features/filters.html" + target="_blank"> + <i className="fa fa-external-link"></i> + mitmproxy docs</a> + </td> + </tr>); + return <table className="table table-condensed"> + <tbody>{commands}</tbody> + </table>; + } + } +}); +var FilterInput = React.createClass({ + mixins: [common.ChildFocus], + getInitialState: function () { + // Consider both focus and mouseover for showing/hiding the tooltip, + // because onBlur of the input is triggered before the click on the tooltip + // finalized, hiding the tooltip just as the user clicks on it. + return { + value: this.props.value, + focus: false, + mousefocus: false + }; + }, + componentWillReceiveProps: function (nextProps) { + this.setState({value: nextProps.value}); + }, + onChange: function (e) { + var nextValue = e.target.value; + this.setState({ + value: nextValue + }); + // Only propagate valid filters upwards. + if (this.isValid(nextValue)) { + this.props.onChange(nextValue); + } + }, + isValid: function (filt) { + try { + Filt.parse(filt || this.state.value); + return true; + } catch (e) { + return false; + } + }, + getDesc: function () { + var desc; + try { + desc = Filt.parse(this.state.value).desc; + } catch (e) { + desc = "" + e; + } + if (desc !== "true") { + return desc; + } else { + return ( + <FilterDocs/> + ); + } + }, + onFocus: function () { + this.setState({focus: true}); + }, + onBlur: function () { + this.setState({focus: false}); + }, + onMouseEnter: function () { + this.setState({mousefocus: true}); + }, + onMouseLeave: function () { + this.setState({mousefocus: false}); + }, + onKeyDown: function (e) { + if (e.keyCode === utils.Key.ESC || e.keyCode === utils.Key.ENTER) { + this.blur(); + // If closed using ESC/ENTER, hide the tooltip. + this.setState({mousefocus: false}); + } + e.stopPropagation(); + }, + blur: function () { + this.refs.input.getDOMNode().blur(); + this.returnFocus(); + }, + select: function () { + this.refs.input.getDOMNode().select(); + }, + render: function () { + var isValid = this.isValid(); + var icon = "fa fa-fw fa-" + this.props.type; + var groupClassName = "filter-input input-group" + (isValid ? "" : " has-error"); + + var popover; + if (this.state.focus || this.state.mousefocus) { + popover = ( + <div className="popover bottom" onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> + <div className="arrow"></div> + <div className="popover-content"> + {this.getDesc()} + </div> + </div> + ); + } + + return ( + <div className={groupClassName}> + <span className="input-group-addon"> + <i className={icon} style={{color: this.props.color}}></i> + </span> + <input type="text" placeholder={this.props.placeholder} className="form-control" + ref="input" + onChange={this.onChange} + onFocus={this.onFocus} + onBlur={this.onBlur} + onKeyDown={this.onKeyDown} + value={this.state.value}/> + {popover} + </div> + ); + } +}); + +var MainMenu = React.createClass({ + mixins: [common.Navigation, common.RouterState, common.SettingsState], + statics: { + title: "Start", + route: "flows" + }, + onSearchChange: function (val) { + var d = {}; + d[Query.SEARCH] = val; + this.setQuery(d); + }, + onHighlightChange: function (val) { + var d = {}; + d[Query.HIGHLIGHT] = val; + this.setQuery(d); + }, + onInterceptChange: function (val) { + actions.SettingsActions.update({intercept: val}); + }, + render: function () { + var search = this.getQuery()[Query.SEARCH] || ""; + var highlight = this.getQuery()[Query.HIGHLIGHT] || ""; + var intercept = this.state.settings.intercept || ""; + + return ( + <div> + <div className="menu-row"> + <FilterInput + ref="search" + placeholder="Search" + type="search" + color="black" + value={search} + onChange={this.onSearchChange} /> + <FilterInput + ref="highlight" + placeholder="Highlight" + type="tag" + color="hsl(48, 100%, 50%)" + value={highlight} + onChange={this.onHighlightChange}/> + <FilterInput + ref="intercept" + placeholder="Intercept" + type="pause" + color="hsl(208, 56%, 53%)" + value={intercept} + onChange={this.onInterceptChange}/> + </div> + <div className="clearfix"></div> + </div> + ); + } +}); + + +var ViewMenu = React.createClass({ + statics: { + title: "View", + route: "flows" + }, + mixins: [common.Navigation, common.RouterState], + toggleEventLog: function () { + var d = {}; + + if (this.getQuery()[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.setQuery(d); + }, + render: function () { + var showEventLog = this.getQuery()[Query.SHOW_EVENTLOG]; + return ( + <div> + <button + className={"btn " + (showEventLog ? "btn-primary" : "btn-default")} + onClick={this.toggleEventLog}> + <i className="fa fa-database"></i> + Show Eventlog + </button> + <span> </span> + </div> + ); + } +}); + + +var ReportsMenu = React.createClass({ + statics: { + title: "Visualization", + route: "reports" + }, + render: function () { + return <div>Reports Menu</div>; + } +}); + +var FileMenu = React.createClass({ + getInitialState: function () { + return { + showFileMenu: false + }; + }, + handleFileClick: function (e) { + e.preventDefault(); + if (!this.state.showFileMenu) { + var close = function () { + this.setState({showFileMenu: false}); + document.removeEventListener("click", close); + }.bind(this); + document.addEventListener("click", close); + + this.setState({ + showFileMenu: true + }); + } + }, + handleNewClick: function (e) { + e.preventDefault(); + if (confirm("Delete all flows?")) { + actions.FlowActions.clear(); + } + }, + handleOpenClick: function (e) { + e.preventDefault(); + console.error("unimplemented: handleOpenClick"); + }, + handleSaveClick: function (e) { + e.preventDefault(); + console.error("unimplemented: handleSaveClick"); + }, + handleShutdownClick: function (e) { + e.preventDefault(); + console.error("unimplemented: handleShutdownClick"); + }, + render: function () { + var fileMenuClass = "dropdown pull-left" + (this.state.showFileMenu ? " open" : ""); + + return ( + <div className={fileMenuClass}> + <a href="#" className="special" onClick={this.handleFileClick}> mitmproxy </a> + <ul className="dropdown-menu" role="menu"> + <li> + <a href="#" onClick={this.handleNewClick}> + <i className="fa fa-fw fa-file"></i> + New + </a> + </li> + <li role="presentation" className="divider"></li> + <li> + <a href="http://mitm.it/" target="_blank"> + <i className="fa fa-fw fa-external-link"></i> + Install Certificates... + </a> + </li> + {/* + <li> + <a href="#" onClick={this.handleOpenClick}> + <i className="fa fa-fw fa-folder-open"></i> + Open + </a> + </li> + <li> + <a href="#" onClick={this.handleSaveClick}> + <i className="fa fa-fw fa-save"></i> + Save + </a> + </li> + <li role="presentation" className="divider"></li> + <li> + <a href="#" onClick={this.handleShutdownClick}> + <i className="fa fa-fw fa-plug"></i> + Shutdown + </a> + </li> + */} + </ul> + </div> + ); + } +}); + + +var header_entries = [MainMenu, ViewMenu /*, ReportsMenu */]; + + +var Header = React.createClass({ + mixins: [common.Navigation], + getInitialState: function () { + return { + active: header_entries[0] + }; + }, + handleClick: function (active, e) { + e.preventDefault(); + this.replaceWith(active.route); + this.setState({active: active}); + }, + render: function () { + var header = header_entries.map(function (entry, i) { + var className; + if (entry === this.state.active) { + className = "active"; + } else { + className = ""; + } + return ( + <a key={i} + href="#" + className={className} + onClick={this.handleClick.bind(this, entry)}> + { entry.title} + </a> + ); + }.bind(this)); + + return ( + <header> + <nav className="nav-tabs nav-tabs-lg"> + <FileMenu/> + {header} + </nav> + <div className="menu"> + <this.state.active ref="active"/> + </div> + </header> + ); + } +}); + + +module.exports = { + Header: Header, + MainMenu: MainMenu +};
\ No newline at end of file diff --git a/web/src/js/components/mainview.js b/web/src/js/components/mainview.js new file mode 100644 index 00000000..9ff51dfa --- /dev/null +++ b/web/src/js/components/mainview.js @@ -0,0 +1,244 @@ +var React = require("react"); + +var actions = require("../actions.js"); +var Query = require("../actions.js").Query; +var utils = require("../utils.js"); +var views = require("../store/view.js"); +var Filt = require("../filt/filt.js"); + +var common = require("./common.js"); +var FlowTable = require("./flowtable.js"); +var FlowView = require("./flowview/index.js"); + +var MainView = React.createClass({ + mixins: [common.Navigation, common.RouterState], + contextTypes: { + flowStore: React.PropTypes.object.isRequired, + }, + childContextTypes: { + view: React.PropTypes.object.isRequired, + }, + getChildContext: function () { + return { + view: this.state.view + }; + }, + getInitialState: function () { + var sortKeyFun = false; + var view = new views.StoreView(this.context.flowStore, this.getViewFilt(), sortKeyFun); + view.addListener("recalculate", this.onRecalculate); + view.addListener("add", this.onUpdate); + view.addListener("update", this.onUpdate); + view.addListener("remove", this.onUpdate); + view.addListener("remove", this.onRemove); + + return { + view: view, + sortKeyFun: sortKeyFun + }; + }, + componentWillUnmount: function () { + this.state.view.close(); + }, + getViewFilt: function () { + try { + var filt = Filt.parse(this.getQuery()[Query.SEARCH] || ""); + var highlightStr = this.getQuery()[Query.HIGHLIGHT]; + var highlight = highlightStr ? Filt.parse(highlightStr) : false; + } catch (e) { + console.error("Error when processing filter: " + e); + } + + return function filter_and_highlight(flow) { + if (!this._highlight) { + this._highlight = {}; + } + this._highlight[flow.id] = highlight && highlight(flow); + return filt(flow); + }; + }, + componentWillReceiveProps: function (nextProps) { + var filterChanged = (this.props.query[Query.SEARCH] !== nextProps.query[Query.SEARCH]); + var highlightChanged = (this.props.query[Query.HIGHLIGHT] !== nextProps.query[Query.HIGHLIGHT]); + if (filterChanged || highlightChanged) { + this.state.view.recalculate(this.getViewFilt(), this.state.sortKeyFun); + } + }, + onRecalculate: function () { + this.forceUpdate(); + var selected = this.getSelected(); + if (selected) { + this.refs.flowTable.scrollIntoView(selected); + } + }, + onUpdate: function (flow) { + if (flow.id === this.getParams().flowId) { + this.forceUpdate(); + } + }, + onRemove: function (flow_id, index) { + if (flow_id === this.getParams().flowId) { + var flow_to_select = this.state.view.list[Math.min(index, this.state.view.list.length - 1)]; + this.selectFlow(flow_to_select); + } + }, + setSortKeyFun: function (sortKeyFun) { + this.setState({ + sortKeyFun: sortKeyFun + }); + this.state.view.recalculate(this.getViewFilt(), sortKeyFun); + }, + selectFlow: function (flow) { + if (flow) { + this.replaceWith( + "flow", + { + flowId: flow.id, + detailTab: this.getParams().detailTab || "request" + } + ); + this.refs.flowTable.scrollIntoView(flow); + } else { + this.replaceWith("flows", {}); + } + }, + selectFlowRelative: function (shift) { + var flows = this.state.view.list; + var index; + if (!this.getParams().flowId) { + if (shift < 0) { + index = flows.length - 1; + } else { + index = 0; + } + } else { + var currFlowId = this.getParams().flowId; + var i = flows.length; + while (i--) { + if (flows[i].id === currFlowId) { + index = i; + break; + } + } + index = Math.min( + Math.max(0, index + shift), + flows.length - 1); + } + this.selectFlow(flows[index]); + }, + onMainKeyDown: function (e) { + var flow = this.getSelected(); + if (e.ctrlKey) { + return; + } + switch (e.keyCode) { + case utils.Key.K: + case utils.Key.UP: + this.selectFlowRelative(-1); + break; + case utils.Key.J: + case utils.Key.DOWN: + this.selectFlowRelative(+1); + break; + case utils.Key.SPACE: + case utils.Key.PAGE_DOWN: + this.selectFlowRelative(+10); + break; + case utils.Key.PAGE_UP: + this.selectFlowRelative(-10); + break; + case utils.Key.END: + this.selectFlowRelative(+1e10); + break; + case utils.Key.HOME: + this.selectFlowRelative(-1e10); + break; + case utils.Key.ESC: + this.selectFlow(null); + break; + case utils.Key.H: + case utils.Key.LEFT: + if (this.refs.flowDetails) { + this.refs.flowDetails.nextTab(-1); + } + break; + case utils.Key.L: + case utils.Key.TAB: + case utils.Key.RIGHT: + if (this.refs.flowDetails) { + this.refs.flowDetails.nextTab(+1); + } + break; + case utils.Key.C: + if (e.shiftKey) { + actions.FlowActions.clear(); + } + break; + case utils.Key.D: + if (flow) { + if (e.shiftKey) { + actions.FlowActions.duplicate(flow); + } else { + actions.FlowActions.delete(flow); + } + } + break; + case utils.Key.A: + if (e.shiftKey) { + actions.FlowActions.accept_all(); + } else if (flow && flow.intercepted) { + actions.FlowActions.accept(flow); + } + break; + case utils.Key.R: + if (!e.shiftKey && flow) { + actions.FlowActions.replay(flow); + } + break; + case utils.Key.V: + if (e.shiftKey && flow && flow.modified) { + actions.FlowActions.revert(flow); + } + break; + case utils.Key.E: + if (this.refs.flowDetails) { + this.refs.flowDetails.promptEdit(); + } + break; + case utils.Key.SHIFT: + break; + default: + console.debug("keydown", e.keyCode); + return; + } + e.preventDefault(); + }, + getSelected: function () { + return this.context.flowStore.get(this.getParams().flowId); + }, + render: function () { + var selected = this.getSelected(); + + var details; + if (selected) { + details = [ + <common.Splitter key="splitter"/>, + <FlowView key="flowDetails" ref="flowDetails" flow={selected}/> + ]; + } else { + details = null; + } + + return ( + <div className="main-view"> + <FlowTable ref="flowTable" + selectFlow={this.selectFlow} + setSortKeyFun={this.setSortKeyFun} + selected={selected} /> + {details} + </div> + ); + } +}); + +module.exports = MainView; diff --git a/web/src/js/components/prompt.js b/web/src/js/components/prompt.js new file mode 100644 index 00000000..121a1170 --- /dev/null +++ b/web/src/js/components/prompt.js @@ -0,0 +1,100 @@ +var React = require("react"); +var _ = require("lodash"); + +var utils = require("../utils.js"); +var common = require("./common.js"); + +var Prompt = React.createClass({ + mixins: [common.ChildFocus], + propTypes: { + options: React.PropTypes.array.isRequired, + done: React.PropTypes.func.isRequired, + prompt: React.PropTypes.string + }, + componentDidMount: function () { + React.findDOMNode(this).focus(); + }, + onKeyDown: function (e) { + e.stopPropagation(); + e.preventDefault(); + var opts = this.getOptions(); + for (var i = 0; i < opts.length; i++) { + var k = opts[i].key; + if (utils.Key[k.toUpperCase()] === e.keyCode) { + this.done(k); + return; + } + } + if (e.keyCode === utils.Key.ESC || e.keyCode === utils.Key.ENTER) { + this.done(false); + } + }, + onClick: function (e) { + this.done(false); + }, + done: function (ret) { + this.props.done(ret); + this.returnFocus(); + }, + getOptions: function () { + var opts = []; + + var keyTaken = function (k) { + return _.includes(_.pluck(opts, "key"), k); + }; + + for (var i = 0; i < this.props.options.length; i++) { + var opt = this.props.options[i]; + if (_.isString(opt)) { + var str = opt; + while (str.length > 0 && keyTaken(str[0])) { + str = str.substr(1); + } + opt = { + text: opt, + key: str[0] + }; + } + if (!opt.text || !opt.key || keyTaken(opt.key)) { + throw "invalid options"; + } else { + opts.push(opt); + } + } + return opts; + }, + render: function () { + var opts = this.getOptions(); + opts = _.map(opts, function (o) { + var prefix, suffix; + var idx = o.text.indexOf(o.key); + if (idx !== -1) { + prefix = o.text.substring(0, idx); + suffix = o.text.substring(idx + 1); + + } else { + prefix = o.text + " ("; + suffix = ")"; + } + var onClick = function (e) { + this.done(o.key); + e.stopPropagation(); + }.bind(this); + return <span + key={o.key} + className="option" + onClick={onClick}> + {prefix} + <strong className="text-primary">{o.key}</strong>{suffix} + </span>; + }.bind(this)); + return <div tabIndex="0" onKeyDown={this.onKeyDown} onClick={this.onClick} className="prompt-dialog"> + <div className="prompt-content"> + {this.props.prompt || <strong>Select: </strong> } + {opts} + </div> + </div>; + } +}); + +module.exports = Prompt;
\ No newline at end of file diff --git a/web/src/js/components/proxyapp.js b/web/src/js/components/proxyapp.js new file mode 100644 index 00000000..e766d6e6 --- /dev/null +++ b/web/src/js/components/proxyapp.js @@ -0,0 +1,129 @@ +var React = require("react"); +var ReactRouter = require("react-router"); +var _ = require("lodash"); + +var common = require("./common.js"); +var MainView = require("./mainview.js"); +var Footer = require("./footer.js"); +var header = require("./header.js"); +var EventLog = require("./eventlog.js"); +var store = require("../store/store.js"); +var Query = require("../actions.js").Query; +var Key = require("../utils.js").Key; + + +//TODO: Move out of here, just a stub. +var Reports = React.createClass({ + render: function () { + return <div>ReportEditor</div>; + } +}); + + +var ProxyAppMain = React.createClass({ + mixins: [common.RouterState], + childContextTypes: { + settingsStore: React.PropTypes.object.isRequired, + flowStore: React.PropTypes.object.isRequired, + eventStore: React.PropTypes.object.isRequired, + returnFocus: React.PropTypes.func.isRequired, + }, + componentDidMount: function () { + this.focus(); + }, + getChildContext: function () { + return { + settingsStore: this.state.settingsStore, + flowStore: this.state.flowStore, + eventStore: this.state.eventStore, + returnFocus: this.focus, + }; + }, + getInitialState: function () { + var eventStore = new store.EventLogStore(); + var flowStore = new store.FlowStore(); + var settingsStore = new store.SettingsStore(); + + // Default Settings before fetch + _.extend(settingsStore.dict, {}); + return { + settingsStore: settingsStore, + flowStore: flowStore, + eventStore: eventStore + }; + }, + focus: function () { + React.findDOMNode(this).focus(); + }, + getMainComponent: function () { + return this.refs.view.refs.__routeHandler__; + }, + onKeydown: function (e) { + + var selectFilterInput = function (name) { + var headerComponent = this.refs.header; + headerComponent.setState({active: header.MainMenu}, function () { + headerComponent.refs.active.refs[name].select(); + }); + }.bind(this); + + switch (e.keyCode) { + case Key.I: + selectFilterInput("intercept"); + break; + case Key.L: + selectFilterInput("search"); + break; + case Key.H: + selectFilterInput("highlight"); + break; + default: + var main = this.getMainComponent(); + if (main.onMainKeyDown) { + main.onMainKeyDown(e); + } + return; // don't prevent default then + } + e.preventDefault(); + }, + render: function () { + var eventlog; + if (this.getQuery()[Query.SHOW_EVENTLOG]) { + eventlog = [ + <common.Splitter key="splitter" axis="y"/>, + <EventLog key="eventlog"/> + ]; + } else { + eventlog = null; + } + return ( + <div id="container" tabIndex="0" onKeyDown={this.onKeydown}> + <header.Header ref="header"/> + <RouteHandler ref="view" query={this.getQuery()}/> + {eventlog} + <Footer/> + </div> + ); + } +}); + + +var Route = ReactRouter.Route; +var RouteHandler = ReactRouter.RouteHandler; +var Redirect = ReactRouter.Redirect; +var DefaultRoute = ReactRouter.DefaultRoute; +var NotFoundRoute = ReactRouter.NotFoundRoute; + + +var routes = ( + <Route path="/" handler={ProxyAppMain}> + <Route name="flows" path="flows" handler={MainView}/> + <Route name="flow" path="flows/:flowId/:detailTab" handler={MainView}/> + <Route name="reports" handler={Reports}/> + <Redirect path="/" to="flows" /> + </Route> +); + +module.exports = { + routes: routes +};
\ No newline at end of file diff --git a/web/src/js/components/virtualscroll.js b/web/src/js/components/virtualscroll.js new file mode 100644 index 00000000..956e1a0b --- /dev/null +++ b/web/src/js/components/virtualscroll.js @@ -0,0 +1,85 @@ +var React = require("react"); + +var VirtualScrollMixin = { + getInitialState: function () { + return { + start: 0, + stop: 0 + }; + }, + componentWillMount: function () { + if (!this.props.rowHeight) { + console.warn("VirtualScrollMixin: No rowHeight specified", this); + } + }, + getPlaceholderTop: function (total) { + var Tag = this.props.placeholderTagName || "tr"; + // When a large trunk of elements is removed from the button, start may be far off the viewport. + // To make this issue less severe, limit the top placeholder to the total number of rows. + var style = { + height: Math.min(this.state.start, total) * this.props.rowHeight + }; + var spacer = <Tag key="placeholder-top" style={style}></Tag>; + + if (this.state.start % 2 === 1) { + // fix even/odd rows + return [spacer, <Tag key="placeholder-top-2"></Tag>]; + } else { + return spacer; + } + }, + getPlaceholderBottom: function (total) { + var Tag = this.props.placeholderTagName || "tr"; + var style = { + height: Math.max(0, total - this.state.stop) * this.props.rowHeight + }; + return <Tag key="placeholder-bottom" style={style}></Tag>; + }, + componentDidMount: function () { + this.onScroll(); + window.addEventListener('resize', this.onScroll); + }, + componentWillUnmount: function(){ + window.removeEventListener('resize', this.onScroll); + }, + onScroll: function () { + var viewport = this.getDOMNode(); + var top = viewport.scrollTop; + var height = viewport.offsetHeight; + var start = Math.floor(top / this.props.rowHeight); + var stop = start + Math.ceil(height / (this.props.rowHeightMin || this.props.rowHeight)); + + this.setState({ + start: start, + stop: stop + }); + }, + renderRows: function (elems) { + var rows = []; + var max = Math.min(elems.length, this.state.stop); + + for (var i = this.state.start; i < max; i++) { + var elem = elems[i]; + rows.push(this.renderRow(elem)); + } + return rows; + }, + scrollRowIntoView: function (index, head_height) { + + var row_top = (index * this.props.rowHeight) + head_height; + var row_bottom = row_top + this.props.rowHeight; + + var viewport = this.getDOMNode(); + var viewport_top = viewport.scrollTop; + var viewport_bottom = viewport_top + viewport.offsetHeight; + + // Account for pinned thead + if (row_top - head_height < viewport_top) { + viewport.scrollTop = row_top - head_height; + } else if (row_bottom > viewport_bottom) { + viewport.scrollTop = row_bottom - viewport.offsetHeight; + } + }, +}; + +module.exports = VirtualScrollMixin;
\ No newline at end of file diff --git a/web/src/js/connection.js b/web/src/js/connection.js new file mode 100644 index 00000000..5e229b6e --- /dev/null +++ b/web/src/js/connection.js @@ -0,0 +1,29 @@ + +var actions = require("./actions.js"); +var AppDispatcher = require("./dispatcher.js").AppDispatcher; + +function Connection(url) { + if (url[0] === "/") { + url = location.origin.replace("http", "ws") + url; + } + + var ws = new WebSocket(url); + ws.onopen = function () { + actions.ConnectionActions.open(); + }; + ws.onmessage = function (message) { + var m = JSON.parse(message.data); + AppDispatcher.dispatchServerAction(m); + }; + ws.onerror = function () { + actions.ConnectionActions.error(); + actions.EventLogActions.add_event("WebSocket connection error."); + }; + ws.onclose = function () { + actions.ConnectionActions.close(); + actions.EventLogActions.add_event("WebSocket connection closed."); + }; + return ws; +} + +module.exports = Connection;
\ No newline at end of file diff --git a/web/src/js/dispatcher.js b/web/src/js/dispatcher.js new file mode 100644 index 00000000..0c2aa202 --- /dev/null +++ b/web/src/js/dispatcher.js @@ -0,0 +1,22 @@ + +var flux = require("flux"); + +const PayloadSources = { + VIEW: "view", + SERVER: "server" +}; + + +var AppDispatcher = new flux.Dispatcher(); +AppDispatcher.dispatchViewAction = function (action) { + action.source = PayloadSources.VIEW; + this.dispatch(action); +}; +AppDispatcher.dispatchServerAction = function (action) { + action.source = PayloadSources.SERVER; + this.dispatch(action); +}; + +module.exports = { + AppDispatcher: AppDispatcher +};
\ No newline at end of file diff --git a/web/src/js/filt/filt.js b/web/src/js/filt/filt.js new file mode 100644 index 00000000..45b42f3a --- /dev/null +++ b/web/src/js/filt/filt.js @@ -0,0 +1,1774 @@ +module.exports = (function() { + /* + * Generated by PEG.js 0.8.0. + * + * http://pegjs.majda.cz/ + */ + + function peg$subclass(child, parent) { + function ctor() { this.constructor = child; } + ctor.prototype = parent.prototype; + child.prototype = new ctor(); + } + + function SyntaxError(message, expected, found, offset, line, column) { + this.message = message; + this.expected = expected; + this.found = found; + this.offset = offset; + this.line = line; + this.column = column; + + this.name = "SyntaxError"; + } + + peg$subclass(SyntaxError, Error); + + function parse(input) { + var options = arguments.length > 1 ? arguments[1] : {}, + + peg$FAILED = {}, + + peg$startRuleFunctions = { start: peg$parsestart }, + peg$startRuleFunction = peg$parsestart, + + peg$c0 = { type: "other", description: "filter expression" }, + peg$c1 = peg$FAILED, + peg$c2 = function(orExpr) { return orExpr; }, + peg$c3 = [], + peg$c4 = function() {return trueFilter; }, + peg$c5 = { type: "other", description: "whitespace" }, + peg$c6 = /^[ \t\n\r]/, + peg$c7 = { type: "class", value: "[ \\t\\n\\r]", description: "[ \\t\\n\\r]" }, + peg$c8 = { type: "other", description: "control character" }, + peg$c9 = /^[|&!()~"]/, + peg$c10 = { type: "class", value: "[|&!()~\"]", description: "[|&!()~\"]" }, + peg$c11 = { type: "other", description: "optional whitespace" }, + peg$c12 = "|", + peg$c13 = { type: "literal", value: "|", description: "\"|\"" }, + peg$c14 = function(first, second) { return or(first, second); }, + peg$c15 = "&", + peg$c16 = { type: "literal", value: "&", description: "\"&\"" }, + peg$c17 = function(first, second) { return and(first, second); }, + peg$c18 = "!", + peg$c19 = { type: "literal", value: "!", description: "\"!\"" }, + peg$c20 = function(expr) { return not(expr); }, + peg$c21 = "(", + peg$c22 = { type: "literal", value: "(", description: "\"(\"" }, + peg$c23 = ")", + peg$c24 = { type: "literal", value: ")", description: "\")\"" }, + peg$c25 = function(expr) { return binding(expr); }, + peg$c26 = "~a", + peg$c27 = { type: "literal", value: "~a", description: "\"~a\"" }, + peg$c28 = function() { return assetFilter; }, + peg$c29 = "~e", + peg$c30 = { type: "literal", value: "~e", description: "\"~e\"" }, + peg$c31 = function() { return errorFilter; }, + peg$c32 = "~q", + peg$c33 = { type: "literal", value: "~q", description: "\"~q\"" }, + peg$c34 = function() { return noResponseFilter; }, + peg$c35 = "~s", + peg$c36 = { type: "literal", value: "~s", description: "\"~s\"" }, + peg$c37 = function() { return responseFilter; }, + peg$c38 = "true", + peg$c39 = { type: "literal", value: "true", description: "\"true\"" }, + peg$c40 = function() { return trueFilter; }, + peg$c41 = "false", + peg$c42 = { type: "literal", value: "false", description: "\"false\"" }, + peg$c43 = function() { return falseFilter; }, + peg$c44 = "~c", + peg$c45 = { type: "literal", value: "~c", description: "\"~c\"" }, + peg$c46 = function(s) { return responseCode(s); }, + peg$c47 = "~d", + peg$c48 = { type: "literal", value: "~d", description: "\"~d\"" }, + peg$c49 = function(s) { return domain(s); }, + peg$c50 = "~h", + peg$c51 = { type: "literal", value: "~h", description: "\"~h\"" }, + peg$c52 = function(s) { return header(s); }, + peg$c53 = "~hq", + peg$c54 = { type: "literal", value: "~hq", description: "\"~hq\"" }, + peg$c55 = function(s) { return requestHeader(s); }, + peg$c56 = "~hs", + peg$c57 = { type: "literal", value: "~hs", description: "\"~hs\"" }, + peg$c58 = function(s) { return responseHeader(s); }, + peg$c59 = "~m", + peg$c60 = { type: "literal", value: "~m", description: "\"~m\"" }, + peg$c61 = function(s) { return method(s); }, + peg$c62 = "~t", + peg$c63 = { type: "literal", value: "~t", description: "\"~t\"" }, + peg$c64 = function(s) { return contentType(s); }, + peg$c65 = "~tq", + peg$c66 = { type: "literal", value: "~tq", description: "\"~tq\"" }, + peg$c67 = function(s) { return requestContentType(s); }, + peg$c68 = "~ts", + peg$c69 = { type: "literal", value: "~ts", description: "\"~ts\"" }, + peg$c70 = function(s) { return responseContentType(s); }, + peg$c71 = "~u", + peg$c72 = { type: "literal", value: "~u", description: "\"~u\"" }, + peg$c73 = function(s) { return url(s); }, + peg$c74 = { type: "other", description: "integer" }, + peg$c75 = null, + peg$c76 = /^['"]/, + peg$c77 = { type: "class", value: "['\"]", description: "['\"]" }, + peg$c78 = /^[0-9]/, + peg$c79 = { type: "class", value: "[0-9]", description: "[0-9]" }, + peg$c80 = function(digits) { return parseInt(digits.join(""), 10); }, + peg$c81 = { type: "other", description: "string" }, + peg$c82 = "\"", + peg$c83 = { type: "literal", value: "\"", description: "\"\\\"\"" }, + peg$c84 = function(chars) { return chars.join(""); }, + peg$c85 = "'", + peg$c86 = { type: "literal", value: "'", description: "\"'\"" }, + peg$c87 = void 0, + peg$c88 = /^["\\]/, + peg$c89 = { type: "class", value: "[\"\\\\]", description: "[\"\\\\]" }, + peg$c90 = { type: "any", description: "any character" }, + peg$c91 = function(char) { return char; }, + peg$c92 = "\\", + peg$c93 = { type: "literal", value: "\\", description: "\"\\\\\"" }, + peg$c94 = /^['\\]/, + peg$c95 = { type: "class", value: "['\\\\]", description: "['\\\\]" }, + peg$c96 = /^['"\\]/, + peg$c97 = { type: "class", value: "['\"\\\\]", description: "['\"\\\\]" }, + peg$c98 = "n", + peg$c99 = { type: "literal", value: "n", description: "\"n\"" }, + peg$c100 = function() { return "\n"; }, + peg$c101 = "r", + peg$c102 = { type: "literal", value: "r", description: "\"r\"" }, + peg$c103 = function() { return "\r"; }, + peg$c104 = "t", + peg$c105 = { type: "literal", value: "t", description: "\"t\"" }, + peg$c106 = function() { return "\t"; }, + + peg$currPos = 0, + peg$reportedPos = 0, + peg$cachedPos = 0, + peg$cachedPosDetails = { line: 1, column: 1, seenCR: false }, + peg$maxFailPos = 0, + peg$maxFailExpected = [], + peg$silentFails = 0, + + peg$result; + + if ("startRule" in options) { + if (!(options.startRule in peg$startRuleFunctions)) { + throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); + } + + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; + } + + function text() { + return input.substring(peg$reportedPos, peg$currPos); + } + + function offset() { + return peg$reportedPos; + } + + function line() { + return peg$computePosDetails(peg$reportedPos).line; + } + + function column() { + return peg$computePosDetails(peg$reportedPos).column; + } + + function expected(description) { + throw peg$buildException( + null, + [{ type: "other", description: description }], + peg$reportedPos + ); + } + + function error(message) { + throw peg$buildException(message, null, peg$reportedPos); + } + + function peg$computePosDetails(pos) { + function advance(details, startPos, endPos) { + var p, ch; + + for (p = startPos; p < endPos; p++) { + ch = input.charAt(p); + if (ch === "\n") { + if (!details.seenCR) { details.line++; } + details.column = 1; + details.seenCR = false; + } else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") { + details.line++; + details.column = 1; + details.seenCR = true; + } else { + details.column++; + details.seenCR = false; + } + } + } + + if (peg$cachedPos !== pos) { + if (peg$cachedPos > pos) { + peg$cachedPos = 0; + peg$cachedPosDetails = { line: 1, column: 1, seenCR: false }; + } + advance(peg$cachedPosDetails, peg$cachedPos, pos); + peg$cachedPos = pos; + } + + return peg$cachedPosDetails; + } + + function peg$fail(expected) { + if (peg$currPos < peg$maxFailPos) { return; } + + if (peg$currPos > peg$maxFailPos) { + peg$maxFailPos = peg$currPos; + peg$maxFailExpected = []; + } + + peg$maxFailExpected.push(expected); + } + + function peg$buildException(message, expected, pos) { + function cleanupExpected(expected) { + var i = 1; + + expected.sort(function(a, b) { + if (a.description < b.description) { + return -1; + } else if (a.description > b.description) { + return 1; + } else { + return 0; + } + }); + + while (i < expected.length) { + if (expected[i - 1] === expected[i]) { + expected.splice(i, 1); + } else { + i++; + } + } + } + + function buildMessage(expected, found) { + function stringEscape(s) { + function hex(ch) { return ch.charCodeAt(0).toString(16).toUpperCase(); } + + return s + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\x08/g, '\\b') + .replace(/\t/g, '\\t') + .replace(/\n/g, '\\n') + .replace(/\f/g, '\\f') + .replace(/\r/g, '\\r') + .replace(/[\x00-\x07\x0B\x0E\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) + .replace(/[\x10-\x1F\x80-\xFF]/g, function(ch) { return '\\x' + hex(ch); }) + .replace(/[\u0180-\u0FFF]/g, function(ch) { return '\\u0' + hex(ch); }) + .replace(/[\u1080-\uFFFF]/g, function(ch) { return '\\u' + hex(ch); }); + } + + var expectedDescs = new Array(expected.length), + expectedDesc, foundDesc, i; + + for (i = 0; i < expected.length; i++) { + expectedDescs[i] = expected[i].description; + } + + expectedDesc = expected.length > 1 + ? expectedDescs.slice(0, -1).join(", ") + + " or " + + expectedDescs[expected.length - 1] + : expectedDescs[0]; + + foundDesc = found ? "\"" + stringEscape(found) + "\"" : "end of input"; + + return "Expected " + expectedDesc + " but " + foundDesc + " found."; + } + + var posDetails = peg$computePosDetails(pos), + found = pos < input.length ? input.charAt(pos) : null; + + if (expected !== null) { + cleanupExpected(expected); + } + + return new SyntaxError( + message !== null ? message : buildMessage(expected, found), + expected, + found, + pos, + posDetails.line, + posDetails.column + ); + } + + function peg$parsestart() { + var s0, s1, s2, s3; + + peg$silentFails++; + s0 = peg$currPos; + s1 = peg$parse__(); + if (s1 !== peg$FAILED) { + s2 = peg$parseOrExpr(); + if (s2 !== peg$FAILED) { + s3 = peg$parse__(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c2(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = []; + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c4(); + } + s0 = s1; + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + + return s0; + } + + function peg$parsews() { + var s0, s1; + + peg$silentFails++; + if (peg$c6.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c7); } + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c5); } + } + + return s0; + } + + function peg$parsecc() { + var s0, s1; + + peg$silentFails++; + if (peg$c9.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c10); } + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c8); } + } + + return s0; + } + + function peg$parse__() { + var s0, s1; + + peg$silentFails++; + s0 = []; + s1 = peg$parsews(); + while (s1 !== peg$FAILED) { + s0.push(s1); + s1 = peg$parsews(); + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c11); } + } + + return s0; + } + + function peg$parseOrExpr() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parseAndExpr(); + if (s1 !== peg$FAILED) { + s2 = peg$parse__(); + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 124) { + s3 = peg$c12; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c13); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parse__(); + if (s4 !== peg$FAILED) { + s5 = peg$parseOrExpr(); + if (s5 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c14(s1, s5); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$parseAndExpr(); + } + + return s0; + } + + function peg$parseAndExpr() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parseNotExpr(); + if (s1 !== peg$FAILED) { + s2 = peg$parse__(); + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 38) { + s3 = peg$c15; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parse__(); + if (s4 !== peg$FAILED) { + s5 = peg$parseAndExpr(); + if (s5 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c17(s1, s5); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parseNotExpr(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseAndExpr(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c17(s1, s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$parseNotExpr(); + } + } + + return s0; + } + + function peg$parseNotExpr() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 33) { + s1 = peg$c18; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c19); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parse__(); + if (s2 !== peg$FAILED) { + s3 = peg$parseNotExpr(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c20(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$parseBindingExpr(); + } + + return s0; + } + + function peg$parseBindingExpr() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 40) { + s1 = peg$c21; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c22); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parse__(); + if (s2 !== peg$FAILED) { + s3 = peg$parseOrExpr(); + if (s3 !== peg$FAILED) { + s4 = peg$parse__(); + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 41) { + s5 = peg$c23; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c24); } + } + if (s5 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c25(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$parseExpr(); + } + + return s0; + } + + function peg$parseExpr() { + var s0; + + s0 = peg$parseNullaryExpr(); + if (s0 === peg$FAILED) { + s0 = peg$parseUnaryExpr(); + } + + return s0; + } + + function peg$parseNullaryExpr() { + var s0, s1; + + s0 = peg$parseBooleanLiteral(); + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c26) { + s1 = peg$c26; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c27); } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c28(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c29) { + s1 = peg$c29; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c30); } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c31(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c32) { + s1 = peg$c32; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c33); } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c34(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c35) { + s1 = peg$c35; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c36); } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c37(); + } + s0 = s1; + } + } + } + } + + return s0; + } + + function peg$parseBooleanLiteral() { + var s0, s1; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c38) { + s1 = peg$c38; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c39); } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c40(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 5) === peg$c41) { + s1 = peg$c41; + peg$currPos += 5; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c42); } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c43(); + } + s0 = s1; + } + + return s0; + } + + function peg$parseUnaryExpr() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c44) { + s1 = peg$c44; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c45); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseIntegerLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c46(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c47) { + s1 = peg$c47; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c48); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c49(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c50) { + s1 = peg$c50; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c51); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c52(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 3) === peg$c53) { + s1 = peg$c53; + peg$currPos += 3; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c54); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c55(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 3) === peg$c56) { + s1 = peg$c56; + peg$currPos += 3; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c57); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c58(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c59) { + s1 = peg$c59; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c60); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c61(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c62) { + s1 = peg$c62; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c63); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c64(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 3) === peg$c65) { + s1 = peg$c65; + peg$currPos += 3; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c66); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c67(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 3) === peg$c68) { + s1 = peg$c68; + peg$currPos += 3; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c69); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c70(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c71) { + s1 = peg$c71; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c72); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsews(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsews(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseStringLiteral(); + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c73(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parseStringLiteral(); + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c73(s1); + } + s0 = s1; + } + } + } + } + } + } + } + } + } + } + + return s0; + } + + function peg$parseIntegerLiteral() { + var s0, s1, s2, s3; + + peg$silentFails++; + s0 = peg$currPos; + if (peg$c76.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c77); } + } + if (s1 === peg$FAILED) { + s1 = peg$c75; + } + if (s1 !== peg$FAILED) { + s2 = []; + if (peg$c78.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c79); } + } + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + if (peg$c78.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c79); } + } + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + if (peg$c76.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c77); } + } + if (s3 === peg$FAILED) { + s3 = peg$c75; + } + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c80(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c74); } + } + + return s0; + } + + function peg$parseStringLiteral() { + var s0, s1, s2, s3; + + peg$silentFails++; + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 34) { + s1 = peg$c82; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c83); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseDoubleStringChar(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseDoubleStringChar(); + } + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 34) { + s3 = peg$c82; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c83); } + } + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c84(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 39) { + s1 = peg$c85; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c86); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseSingleStringChar(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseSingleStringChar(); + } + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 39) { + s3 = peg$c85; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c86); } + } + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c84(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$currPos; + peg$silentFails++; + s2 = peg$parsecc(); + peg$silentFails--; + if (s2 === peg$FAILED) { + s1 = peg$c87; + } else { + peg$currPos = s1; + s1 = peg$c1; + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseUnquotedStringChar(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseUnquotedStringChar(); + } + } else { + s2 = peg$c1; + } + if (s2 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c84(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c81); } + } + + return s0; + } + + function peg$parseDoubleStringChar() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$currPos; + peg$silentFails++; + if (peg$c88.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c89); } + } + peg$silentFails--; + if (s2 === peg$FAILED) { + s1 = peg$c87; + } else { + peg$currPos = s1; + s1 = peg$c1; + } + if (s1 !== peg$FAILED) { + if (input.length > peg$currPos) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c90); } + } + if (s2 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c91(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c92; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c93); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseEscapeSequence(); + if (s2 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c91(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + + return s0; + } + + function peg$parseSingleStringChar() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$currPos; + peg$silentFails++; + if (peg$c94.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c95); } + } + peg$silentFails--; + if (s2 === peg$FAILED) { + s1 = peg$c87; + } else { + peg$currPos = s1; + s1 = peg$c1; + } + if (s1 !== peg$FAILED) { + if (input.length > peg$currPos) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c90); } + } + if (s2 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c91(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c92; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c93); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseEscapeSequence(); + if (s2 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c91(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + + return s0; + } + + function peg$parseUnquotedStringChar() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$currPos; + peg$silentFails++; + s2 = peg$parsews(); + peg$silentFails--; + if (s2 === peg$FAILED) { + s1 = peg$c87; + } else { + peg$currPos = s1; + s1 = peg$c1; + } + if (s1 !== peg$FAILED) { + if (input.length > peg$currPos) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c90); } + } + if (s2 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c91(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseEscapeSequence() { + var s0, s1; + + if (peg$c96.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c97); } + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 110) { + s1 = peg$c98; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c99); } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c100(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 114) { + s1 = peg$c101; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c102); } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c103(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 116) { + s1 = peg$c104; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c105); } + } + if (s1 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c106(); + } + s0 = s1; + } + } + } + + return s0; + } + + + var flowutils = require("../flow/utils.js"); + + function or(first, second) { + // Add explicit function names to ease debugging. + function orFilter() { + return first.apply(this, arguments) || second.apply(this, arguments); + } + orFilter.desc = first.desc + " or " + second.desc; + return orFilter; + } + function and(first, second) { + function andFilter() { + return first.apply(this, arguments) && second.apply(this, arguments); + } + andFilter.desc = first.desc + " and " + second.desc; + return andFilter; + } + function not(expr) { + function notFilter() { + return !expr.apply(this, arguments); + } + notFilter.desc = "not " + expr.desc; + return notFilter; + } + function binding(expr) { + function bindingFilter() { + return expr.apply(this, arguments); + } + bindingFilter.desc = "(" + expr.desc + ")"; + return bindingFilter; + } + function trueFilter(flow) { + return true; + } + trueFilter.desc = "true"; + function falseFilter(flow) { + return false; + } + falseFilter.desc = "false"; + + var ASSET_TYPES = [ + new RegExp("text/javascript"), + new RegExp("application/x-javascript"), + new RegExp("application/javascript"), + new RegExp("text/css"), + new RegExp("image/.*"), + new RegExp("application/x-shockwave-flash") + ]; + function assetFilter(flow) { + if (flow.response) { + var ct = flowutils.ResponseUtils.getContentType(flow.response); + var i = ASSET_TYPES.length; + while (i--) { + if (ASSET_TYPES[i].test(ct)) { + return true; + } + } + } + return false; + } + assetFilter.desc = "is asset"; + function responseCode(code){ + function responseCodeFilter(flow){ + return flow.response && flow.response.status_code === code; + } + responseCodeFilter.desc = "resp. code is " + code; + return responseCodeFilter; + } + function domain(regex){ + regex = new RegExp(regex, "i"); + function domainFilter(flow){ + return flow.request && regex.test(flow.request.host); + } + domainFilter.desc = "domain matches " + regex; + return domainFilter; + } + function errorFilter(flow){ + return !!flow.error; + } + errorFilter.desc = "has error"; + function header(regex){ + regex = new RegExp(regex, "i"); + function headerFilter(flow){ + return ( + (flow.request && flowutils.RequestUtils.match_header(flow.request, regex)) + || + (flow.response && flowutils.ResponseUtils.match_header(flow.response, regex)) + ); + } + headerFilter.desc = "header matches " + regex; + return headerFilter; + } + function requestHeader(regex){ + regex = new RegExp(regex, "i"); + function requestHeaderFilter(flow){ + return (flow.request && flowutils.RequestUtils.match_header(flow.request, regex)); + } + requestHeaderFilter.desc = "req. header matches " + regex; + return requestHeaderFilter; + } + function responseHeader(regex){ + regex = new RegExp(regex, "i"); + function responseHeaderFilter(flow){ + return (flow.response && flowutils.ResponseUtils.match_header(flow.response, regex)); + } + responseHeaderFilter.desc = "resp. header matches " + regex; + return responseHeaderFilter; + } + function method(regex){ + regex = new RegExp(regex, "i"); + function methodFilter(flow){ + return flow.request && regex.test(flow.request.method); + } + methodFilter.desc = "method matches " + regex; + return methodFilter; + } + function noResponseFilter(flow){ + return flow.request && !flow.response; + } + noResponseFilter.desc = "has no response"; + function responseFilter(flow){ + return !!flow.response; + } + responseFilter.desc = "has response"; + + function contentType(regex){ + regex = new RegExp(regex, "i"); + function contentTypeFilter(flow){ + return ( + (flow.request && regex.test(flowutils.RequestUtils.getContentType(flow.request))) + || + (flow.response && regex.test(flowutils.ResponseUtils.getContentType(flow.response))) + ); + } + contentTypeFilter.desc = "content type matches " + regex; + return contentTypeFilter; + } + function requestContentType(regex){ + regex = new RegExp(regex, "i"); + function requestContentTypeFilter(flow){ + return flow.request && regex.test(flowutils.RequestUtils.getContentType(flow.request)); + } + requestContentTypeFilter.desc = "req. content type matches " + regex; + return requestContentTypeFilter; + } + function responseContentType(regex){ + regex = new RegExp(regex, "i"); + function responseContentTypeFilter(flow){ + return flow.response && regex.test(flowutils.ResponseUtils.getContentType(flow.response)); + } + responseContentTypeFilter.desc = "resp. content type matches " + regex; + return responseContentTypeFilter; + } + function url(regex){ + regex = new RegExp(regex, "i"); + function urlFilter(flow){ + return flow.request && regex.test(flowutils.RequestUtils.pretty_url(flow.request)); + } + urlFilter.desc = "url matches " + regex; + return urlFilter; + } + + + peg$result = peg$startRuleFunction(); + + if (peg$result !== peg$FAILED && peg$currPos === input.length) { + return peg$result; + } else { + if (peg$result !== peg$FAILED && peg$currPos < input.length) { + peg$fail({ type: "end", description: "end of input" }); + } + + throw peg$buildException(null, peg$maxFailExpected, peg$maxFailPos); + } + } + + return { + SyntaxError: SyntaxError, + parse: parse + }; +})();
\ No newline at end of file diff --git a/web/src/js/filt/filt.peg b/web/src/js/filt/filt.peg new file mode 100644 index 00000000..6f9bdac6 --- /dev/null +++ b/web/src/js/filt/filt.peg @@ -0,0 +1,249 @@ +// PEG.js filter rules - see http://pegjs.majda.cz/online + +{ +var flowutils = require("../flow/utils.js"); + +function or(first, second) { + // Add explicit function names to ease debugging. + function orFilter() { + return first.apply(this, arguments) || second.apply(this, arguments); + } + orFilter.desc = first.desc + " or " + second.desc; + return orFilter; +} +function and(first, second) { + function andFilter() { + return first.apply(this, arguments) && second.apply(this, arguments); + } + andFilter.desc = first.desc + " and " + second.desc; + return andFilter; +} +function not(expr) { + function notFilter() { + return !expr.apply(this, arguments); + } + notFilter.desc = "not " + expr.desc; + return notFilter; +} +function binding(expr) { + function bindingFilter() { + return expr.apply(this, arguments); + } + bindingFilter.desc = "(" + expr.desc + ")"; + return bindingFilter; +} +function trueFilter(flow) { + return true; +} +trueFilter.desc = "true"; +function falseFilter(flow) { + return false; +} +falseFilter.desc = "false"; + +var ASSET_TYPES = [ + new RegExp("text/javascript"), + new RegExp("application/x-javascript"), + new RegExp("application/javascript"), + new RegExp("text/css"), + new RegExp("image/.*"), + new RegExp("application/x-shockwave-flash") +]; +function assetFilter(flow) { + if (flow.response) { + var ct = flowutils.ResponseUtils.getContentType(flow.response); + var i = ASSET_TYPES.length; + while (i--) { + if (ASSET_TYPES[i].test(ct)) { + return true; + } + } + } + return false; +} +assetFilter.desc = "is asset"; +function responseCode(code){ + function responseCodeFilter(flow){ + return flow.response && flow.response.status_code === code; + } + responseCodeFilter.desc = "resp. code is " + code; + return responseCodeFilter; +} +function domain(regex){ + regex = new RegExp(regex, "i"); + function domainFilter(flow){ + return flow.request && regex.test(flow.request.host); + } + domainFilter.desc = "domain matches " + regex; + return domainFilter; +} +function errorFilter(flow){ + return !!flow.error; +} +errorFilter.desc = "has error"; +function header(regex){ + regex = new RegExp(regex, "i"); + function headerFilter(flow){ + return ( + (flow.request && flowutils.RequestUtils.match_header(flow.request, regex)) + || + (flow.response && flowutils.ResponseUtils.match_header(flow.response, regex)) + ); + } + headerFilter.desc = "header matches " + regex; + return headerFilter; +} +function requestHeader(regex){ + regex = new RegExp(regex, "i"); + function requestHeaderFilter(flow){ + return (flow.request && flowutils.RequestUtils.match_header(flow.request, regex)); + } + requestHeaderFilter.desc = "req. header matches " + regex; + return requestHeaderFilter; +} +function responseHeader(regex){ + regex = new RegExp(regex, "i"); + function responseHeaderFilter(flow){ + return (flow.response && flowutils.ResponseUtils.match_header(flow.response, regex)); + } + responseHeaderFilter.desc = "resp. header matches " + regex; + return responseHeaderFilter; +} +function method(regex){ + regex = new RegExp(regex, "i"); + function methodFilter(flow){ + return flow.request && regex.test(flow.request.method); + } + methodFilter.desc = "method matches " + regex; + return methodFilter; +} +function noResponseFilter(flow){ + return flow.request && !flow.response; +} +noResponseFilter.desc = "has no response"; +function responseFilter(flow){ + return !!flow.response; +} +responseFilter.desc = "has response"; + +function contentType(regex){ + regex = new RegExp(regex, "i"); + function contentTypeFilter(flow){ + return ( + (flow.request && regex.test(flowutils.RequestUtils.getContentType(flow.request))) + || + (flow.response && regex.test(flowutils.ResponseUtils.getContentType(flow.response))) + ); + } + contentTypeFilter.desc = "content type matches " + regex; + return contentTypeFilter; +} +function requestContentType(regex){ + regex = new RegExp(regex, "i"); + function requestContentTypeFilter(flow){ + return flow.request && regex.test(flowutils.RequestUtils.getContentType(flow.request)); + } + requestContentTypeFilter.desc = "req. content type matches " + regex; + return requestContentTypeFilter; +} +function responseContentType(regex){ + regex = new RegExp(regex, "i"); + function responseContentTypeFilter(flow){ + return flow.response && regex.test(flowutils.ResponseUtils.getContentType(flow.response)); + } + responseContentTypeFilter.desc = "resp. content type matches " + regex; + return responseContentTypeFilter; +} +function url(regex){ + regex = new RegExp(regex, "i"); + function urlFilter(flow){ + return flow.request && regex.test(flowutils.RequestUtils.pretty_url(flow.request)); + } + urlFilter.desc = "url matches " + regex; + return urlFilter; +} +} + +start "filter expression" + = __ orExpr:OrExpr __ { return orExpr; } + / {return trueFilter; } + +ws "whitespace" = [ \t\n\r] +cc "control character" = [|&!()~"] +__ "optional whitespace" = ws* + +OrExpr + = first:AndExpr __ "|" __ second:OrExpr + { return or(first, second); } + / AndExpr + +AndExpr + = first:NotExpr __ "&" __ second:AndExpr + { return and(first, second); } + / first:NotExpr ws+ second:AndExpr + { return and(first, second); } + / NotExpr + +NotExpr + = "!" __ expr:NotExpr + { return not(expr); } + / BindingExpr + +BindingExpr + = "(" __ expr:OrExpr __ ")" + { return binding(expr); } + / Expr + +Expr + = NullaryExpr + / UnaryExpr + +NullaryExpr + = BooleanLiteral + / "~a" { return assetFilter; } + / "~e" { return errorFilter; } + / "~q" { return noResponseFilter; } + / "~s" { return responseFilter; } + + +BooleanLiteral + = "true" { return trueFilter; } + / "false" { return falseFilter; } + +UnaryExpr + = "~c" ws+ s:IntegerLiteral { return responseCode(s); } + / "~d" ws+ s:StringLiteral { return domain(s); } + / "~h" ws+ s:StringLiteral { return header(s); } + / "~hq" ws+ s:StringLiteral { return requestHeader(s); } + / "~hs" ws+ s:StringLiteral { return responseHeader(s); } + / "~m" ws+ s:StringLiteral { return method(s); } + / "~t" ws+ s:StringLiteral { return contentType(s); } + / "~tq" ws+ s:StringLiteral { return requestContentType(s); } + / "~ts" ws+ s:StringLiteral { return responseContentType(s); } + / "~u" ws+ s:StringLiteral { return url(s); } + / s:StringLiteral { return url(s); } + +IntegerLiteral "integer" + = ['"]? digits:[0-9]+ ['"]? { return parseInt(digits.join(""), 10); } + +StringLiteral "string" + = '"' chars:DoubleStringChar* '"' { return chars.join(""); } + / "'" chars:SingleStringChar* "'" { return chars.join(""); } + / !cc chars:UnquotedStringChar+ { return chars.join(""); } + +DoubleStringChar + = !["\\] char:. { return char; } + / "\\" char:EscapeSequence { return char; } + +SingleStringChar + = !['\\] char:. { return char; } + / "\\" char:EscapeSequence { return char; } + +UnquotedStringChar + = !ws char:. { return char; } + +EscapeSequence + = ['"\\] + / "n" { return "\n"; } + / "r" { return "\r"; } + / "t" { return "\t"; } diff --git a/web/src/js/flow/utils.js b/web/src/js/flow/utils.js new file mode 100644 index 00000000..d72febaa --- /dev/null +++ b/web/src/js/flow/utils.js @@ -0,0 +1,130 @@ +var _ = require("lodash"); +var $ = require("jquery"); + +var defaultPorts = { + "http": 80, + "https": 443 +}; + +var MessageUtils = { + getContentType: function (message) { + var ct = this.get_first_header(message, /^Content-Type$/i); + if(ct){ + return ct.split(";")[0].trim(); + } + }, + get_first_header: function (message, regex) { + //FIXME: Cache Invalidation. + if (!message._headerLookups) + Object.defineProperty(message, "_headerLookups", { + value: {}, + configurable: false, + enumerable: false, + writable: false + }); + if (!(regex in message._headerLookups)) { + var header; + for (var i = 0; i < message.headers.length; i++) { + if (!!message.headers[i][0].match(regex)) { + header = message.headers[i]; + break; + } + } + message._headerLookups[regex] = header ? header[1] : undefined; + } + return message._headerLookups[regex]; + }, + match_header: function (message, regex) { + var headers = message.headers; + var i = headers.length; + while (i--) { + if (regex.test(headers[i].join(" "))) { + return headers[i]; + } + } + return false; + }, + getContentURL: function (flow, message) { + if (message === flow.request) { + message = "request"; + } else if (message === flow.response) { + message = "response"; + } + return "/flows/" + flow.id + "/" + message + "/content"; + }, + getContent: function (flow, message) { + var url = MessageUtils.getContentURL(flow, message); + return $.get(url); + } +}; + +var RequestUtils = _.extend(MessageUtils, { + pretty_host: function (request) { + //FIXME: Add hostheader + return request.host; + }, + pretty_url: function (request) { + var port = ""; + if (defaultPorts[request.scheme] !== request.port) { + port = ":" + request.port; + } + return request.scheme + "://" + this.pretty_host(request) + port + request.path; + } +}); + +var ResponseUtils = _.extend(MessageUtils, {}); + + +var parseUrl_regex = /^(?:(https?):\/\/)?([^\/:]+)?(?::(\d+))?(\/.*)?$/i; +var parseUrl = function (url) { + //there are many correct ways to parse a URL, + //however, a mitmproxy user may also wish to generate a not-so-correct URL. ;-) + var parts = parseUrl_regex.exec(url); + if(!parts){ + return false; + } + + var scheme = parts[1], + host = parts[2], + port = parseInt(parts[3]), + path = parts[4]; + if (scheme) { + port = port || defaultPorts[scheme]; + } + var ret = {}; + if (scheme) { + ret.scheme = scheme; + } + if (host) { + ret.host = host; + } + if (port) { + ret.port = port; + } + if (path) { + ret.path = path; + } + return ret; +}; + + +var isValidHttpVersion_regex = /^HTTP\/\d+(\.\d+)*$/i; +var isValidHttpVersion = function (httpVersion) { + return isValidHttpVersion_regex.test(httpVersion); +}; + +var parseHttpVersion = function (httpVersion) { + httpVersion = httpVersion.replace("HTTP/", "").split("."); + return _.map(httpVersion, function (x) { + return parseInt(x); + }); +}; + +module.exports = { + ResponseUtils: ResponseUtils, + RequestUtils: RequestUtils, + MessageUtils: MessageUtils, + parseUrl: parseUrl, + parseHttpVersion: parseHttpVersion, + isValidHttpVersion: isValidHttpVersion +};
\ No newline at end of file diff --git a/web/src/js/store/store.js b/web/src/js/store/store.js new file mode 100644 index 00000000..5024049f --- /dev/null +++ b/web/src/js/store/store.js @@ -0,0 +1,181 @@ + +var _ = require("lodash"); +var $ = require("jquery"); +var EventEmitter = require('events').EventEmitter; + +var utils = require("../utils.js"); +var actions = require("../actions.js"); +var dispatcher = require("../dispatcher.js"); + + +function ListStore() { + EventEmitter.call(this); + this.reset(); +} +_.extend(ListStore.prototype, EventEmitter.prototype, { + add: function (elem) { + if (elem.id in this._pos_map) { + return; + } + this._pos_map[elem.id] = this.list.length; + this.list.push(elem); + this.emit("add", elem); + }, + update: function (elem) { + if (!(elem.id in this._pos_map)) { + return; + } + this.list[this._pos_map[elem.id]] = elem; + this.emit("update", elem); + }, + remove: function (elem_id) { + if (!(elem_id in this._pos_map)) { + return; + } + this.list.splice(this._pos_map[elem_id], 1); + this._build_map(); + this.emit("remove", elem_id); + }, + reset: function (elems) { + this.list = elems || []; + this._build_map(); + this.emit("recalculate"); + }, + _build_map: function () { + this._pos_map = {}; + for (var i = 0; i < this.list.length; i++) { + var elem = this.list[i]; + this._pos_map[elem.id] = i; + } + }, + get: function (elem_id) { + return this.list[this._pos_map[elem_id]]; + }, + index: function (elem_id) { + return this._pos_map[elem_id]; + } +}); + + +function DictStore() { + EventEmitter.call(this); + this.reset(); +} +_.extend(DictStore.prototype, EventEmitter.prototype, { + update: function (dict) { + _.merge(this.dict, dict); + this.emit("recalculate"); + }, + reset: function (dict) { + this.dict = dict || {}; + this.emit("recalculate"); + } +}); + +function LiveStoreMixin(type) { + this.type = type; + + this._updates_before_fetch = undefined; + this._fetchxhr = false; + + this.handle = this.handle.bind(this); + dispatcher.AppDispatcher.register(this.handle); + + // Avoid double-fetch on startup. + if (!(window.ws && window.ws.readyState === WebSocket.CONNECTING)) { + this.fetch(); + } +} +_.extend(LiveStoreMixin.prototype, { + handle: function (event) { + if (event.type === actions.ActionTypes.CONNECTION_OPEN) { + return this.fetch(); + } + if (event.type === this.type) { + if (event.cmd === actions.StoreCmds.RESET) { + this.fetch(event.data); + } else if (this._updates_before_fetch) { + console.log("defer update", event); + this._updates_before_fetch.push(event); + } else { + this[event.cmd](event.data); + } + } + }, + close: function () { + dispatcher.AppDispatcher.unregister(this.handle); + }, + fetch: function (data) { + console.log("fetch " + this.type); + if (this._fetchxhr) { + this._fetchxhr.abort(); + } + this._updates_before_fetch = []; // (JS: empty array is true) + if (data) { + this.handle_fetch(data); + } else { + this._fetchxhr = $.getJSON("/" + this.type) + .done(function (message) { + this.handle_fetch(message.data); + }.bind(this)) + .fail(function () { + EventLogActions.add_event("Could not fetch " + this.type); + }.bind(this)); + } + }, + handle_fetch: function (data) { + this._fetchxhr = false; + console.log(this.type + " fetched.", this._updates_before_fetch); + this.reset(data); + var updates = this._updates_before_fetch; + this._updates_before_fetch = false; + for (var i = 0; i < updates.length; i++) { + this.handle(updates[i]); + } + }, +}); + +function LiveListStore(type) { + ListStore.call(this); + LiveStoreMixin.call(this, type); +} +_.extend(LiveListStore.prototype, ListStore.prototype, LiveStoreMixin.prototype); + +function LiveDictStore(type) { + DictStore.call(this); + LiveStoreMixin.call(this, type); +} +_.extend(LiveDictStore.prototype, DictStore.prototype, LiveStoreMixin.prototype); + + +function FlowStore() { + return new LiveListStore(actions.ActionTypes.FLOW_STORE); +} + +function SettingsStore() { + return new LiveDictStore(actions.ActionTypes.SETTINGS_STORE); +} + +function EventLogStore() { + LiveListStore.call(this, actions.ActionTypes.EVENT_STORE); +} +_.extend(EventLogStore.prototype, LiveListStore.prototype, { + fetch: function(){ + LiveListStore.prototype.fetch.apply(this, arguments); + + // Make sure to display updates even if fetching all events failed. + // This way, we can send "fetch failed" log messages to the log. + if(this._fetchxhr){ + this._fetchxhr.fail(function(){ + this.handle_fetch(null); + }.bind(this)); + } + } +}); + + +module.exports = { + EventLogStore: EventLogStore, + SettingsStore: SettingsStore, + FlowStore: FlowStore +};
\ No newline at end of file diff --git a/web/src/js/store/view.js b/web/src/js/store/view.js new file mode 100644 index 00000000..d628d46b --- /dev/null +++ b/web/src/js/store/view.js @@ -0,0 +1,115 @@ +var EventEmitter = require('events').EventEmitter; +var _ = require("lodash"); + +var utils = require("../utils.js"); + +function SortByStoreOrder(elem) { + return this.store.index(elem.id); +} + +var default_sort = SortByStoreOrder; +var default_filt = function (elem) { + return true; +}; + +function StoreView(store, filt, sortfun) { + EventEmitter.call(this); + + this.store = store; + + this.add = this.add.bind(this); + this.update = this.update.bind(this); + this.remove = this.remove.bind(this); + this.recalculate = this.recalculate.bind(this); + this.store.addListener("add", this.add); + this.store.addListener("update", this.update); + this.store.addListener("remove", this.remove); + this.store.addListener("recalculate", this.recalculate); + + this.recalculate(filt, sortfun); +} + +_.extend(StoreView.prototype, EventEmitter.prototype, { + close: function () { + this.store.removeListener("add", this.add); + this.store.removeListener("update", this.update); + this.store.removeListener("remove", this.remove); + this.store.removeListener("recalculate", this.recalculate); + this.removeAllListeners(); + }, + recalculate: function (filt, sortfun) { + filt = filt || this.filt || default_filt; + sortfun = sortfun || this.sortfun || default_sort; + filt = filt.bind(this); + sortfun = sortfun.bind(this); + this.filt = filt; + this.sortfun = sortfun; + + this.list = this.store.list.filter(filt); + this.list.sort(function (a, b) { + var akey = sortfun(a); + var bkey = sortfun(b); + if(akey < bkey){ + return -1; + } else if(akey > bkey){ + return 1; + } else { + return 0; + } + }); + this.emit("recalculate"); + }, + index: function (elem) { + return _.sortedIndex(this.list, elem, this.sortfun); + }, + add: function (elem) { + if (this.filt(elem)) { + var idx = this.index(elem); + if (idx === this.list.length) { //happens often, .push is way faster. + this.list.push(elem); + } else { + this.list.splice(idx, 0, elem); + } + this.emit("add", elem, idx); + } + }, + update: function (elem) { + var idx; + var i = this.list.length; + // Search from the back, we usually update the latest entries. + while (i--) { + if (this.list[i].id === elem.id) { + idx = i; + break; + } + } + + if (idx === -1) { //not contained in list + this.add(elem); + } else if (!this.filt(elem)) { + this.remove(elem.id); + } else { + if (this.sortfun(this.list[idx]) !== this.sortfun(elem)) { //sortpos has changed + this.remove(this.list[idx]); + this.add(elem); + } else { + this.list[idx] = elem; + this.emit("update", elem, idx); + } + } + }, + remove: function (elem_id) { + var idx = this.list.length; + while (idx--) { + if (this.list[idx].id === elem_id) { + this.list.splice(idx, 1); + this.emit("remove", elem_id, idx); + break; + } + } + } +}); + +module.exports = { + StoreView: StoreView +};
\ No newline at end of file diff --git a/web/src/js/tests/utils.js b/web/src/js/tests/utils.js new file mode 100644 index 00000000..dfbb9ba6 --- /dev/null +++ b/web/src/js/tests/utils.js @@ -0,0 +1,15 @@ +jest.dontMock("jquery"); +jest.dontMock("../utils"); + +describe("utils", function () { + var utils = require("../utils"); + it("formatSize", function(){ + expect(utils.formatSize(1024)).toEqual("1kb"); + expect(utils.formatSize(0)).toEqual("0"); + expect(utils.formatSize(10)).toEqual("10b"); + expect(utils.formatSize(1025)).toEqual("1.0kb"); + expect(utils.formatSize(1024*1024)).toEqual("1mb"); + expect(utils.formatSize(1024*1024+1)).toEqual("1.0mb"); + }); +}); + diff --git a/web/src/js/utils.js b/web/src/js/utils.js new file mode 100644 index 00000000..40575692 --- /dev/null +++ b/web/src/js/utils.js @@ -0,0 +1,112 @@ +var $ = require("jquery"); +var _ = require("lodash"); +var actions = require("./actions.js"); + +window.$ = $; +window._ = _; +window.React = require("react"); + +var Key = { + UP: 38, + DOWN: 40, + PAGE_UP: 33, + PAGE_DOWN: 34, + HOME: 36, + END: 35, + LEFT: 37, + RIGHT: 39, + ENTER: 13, + ESC: 27, + TAB: 9, + SPACE: 32, + BACKSPACE: 8, + SHIFT: 16 +}; +// Add A-Z +for (var i = 65; i <= 90; i++) { + Key[String.fromCharCode(i)] = i; +} + + +var formatSize = function (bytes) { + if (bytes === 0) + return "0"; + var prefix = ["b", "kb", "mb", "gb", "tb"]; + for (var i = 0; i < prefix.length; i++) { + if (Math.pow(1024, i + 1) > bytes) { + break; + } + } + var precision; + if (bytes % Math.pow(1024, i) === 0) + precision = 0; + else + precision = 1; + return (bytes / Math.pow(1024, i)).toFixed(precision) + prefix[i]; +}; + + +var formatTimeDelta = function (milliseconds) { + var time = milliseconds; + var prefix = ["ms", "s", "min", "h"]; + var div = [1000, 60, 60]; + var i = 0; + while (Math.abs(time) >= div[i] && i < div.length) { + time = time / div[i]; + i++; + } + return Math.round(time) + prefix[i]; +}; + + +var formatTimeStamp = function (seconds) { + var ts = (new Date(seconds * 1000)).toISOString(); + return ts.replace("T", " ").replace("Z", ""); +}; + +// At some places, we need to sort strings alphabetically descending, +// but we can only provide a key function. +// This beauty "reverses" a JS string. +var end = String.fromCharCode(0xffff); +function reverseString(s) { + return String.fromCharCode.apply(String, + _.map(s.split(""), function (c) { + return 0xffff - c.charCodeAt(0); + }) + ) + end; +} + +function getCookie(name) { + var r = document.cookie.match(new RegExp("\\b" + name + "=([^;]*)\\b")); + return r ? r[1] : undefined; +} +var xsrf = $.param({_xsrf: getCookie("_xsrf")}); + +//Tornado XSRF Protection. +$.ajaxPrefilter(function (options) { + if (["post", "put", "delete"].indexOf(options.type.toLowerCase()) >= 0 && options.url[0] === "/") { + if(options.url.indexOf("?") === -1){ + options.url += "?" + xsrf; + } else { + options.url += "&" + xsrf; + } + } +}); +// Log AJAX Errors +$(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) { + if (thrownError === "abort") { + return; + } + var message = jqXHR.responseText; + console.error(thrownError, message, arguments); + actions.EventLogActions.add_event(thrownError + ": " + message); + alert(message); +}); + +module.exports = { + formatSize: formatSize, + formatTimeDelta: formatTimeDelta, + formatTimeStamp: formatTimeStamp, + reverseString: reverseString, + Key: Key, +};
\ No newline at end of file |