diff options
Diffstat (limited to 'web/src/js')
| -rw-r--r-- | web/src/js/actions.js | 21 | ||||
| -rw-r--r-- | web/src/js/components/common.js | 8 | ||||
| -rw-r--r-- | web/src/js/components/flowtable-columns.js | 4 | ||||
| -rw-r--r-- | web/src/js/components/flowview/contentview.js | 20 | ||||
| -rw-r--r-- | web/src/js/components/flowview/messages.js | 231 | ||||
| -rw-r--r-- | web/src/js/components/mainview.js | 9 | ||||
| -rw-r--r-- | web/src/js/flow/utils.js | 70 | ||||
| -rw-r--r-- | web/src/js/utils.js | 34 | 
8 files changed, 337 insertions, 60 deletions
diff --git a/web/src/js/actions.js b/web/src/js/actions.js index 78fd4bf7..87c99dd2 100644 --- a/web/src/js/actions.js +++ b/web/src/js/actions.js @@ -1,4 +1,5 @@  var $ = require("jquery"); +var _ = require("lodash");  var AppDispatcher = require("./dispatcher.js").AppDispatcher;  var ActionTypes = { @@ -44,7 +45,8 @@ var SettingsActions = {          $.ajax({              type: "PUT",              url: "/settings", -            data: settings +            contentType: 'application/json', +            data: JSON.stringify(settings)          });          /* @@ -95,15 +97,26 @@ var FlowActions = {      revert: function(flow){          $.post("/flows/" + flow.id + "/revert");      }, -    update: function (flow) { +    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: flow +            data: nextFlow +        }); +        */ +        $.ajax({ +            type: "PUT", +            url: "/flows/" + flow.id, +            contentType: 'application/json', +            data: JSON.stringify(nextProps)          });      },      clear: function(){ -        $.post("/clear"); +        $.post("/flows/" + flow.id);      }  }; diff --git a/web/src/js/components/common.js b/web/src/js/components/common.js index 3ed035ee..956a46d2 100644 --- a/web/src/js/components/common.js +++ b/web/src/js/components/common.js @@ -30,6 +30,13 @@ var StickyHeadMixin = {  }; +var ChildFocus = { +   contextTypes: { +       returnFocus: React.PropTypes.func +   } +}; + +  var Navigation = _.extend({}, ReactRouter.Navigation, {      setQuery: function (dict) {          var q = this.context.router.getCurrentQuery(); @@ -176,6 +183,7 @@ var Splitter = React.createClass({  });  module.exports = { +    ChildFocus: ChildFocus,      State: State,      Navigation: Navigation,      StickyHeadMixin: StickyHeadMixin, diff --git a/web/src/js/components/flowtable-columns.js b/web/src/js/components/flowtable-columns.js index a82c607a..b3b47910 100644 --- a/web/src/js/components/flowtable-columns.js +++ b/web/src/js/components/flowtable-columns.js @@ -16,7 +16,7 @@ var TLSColumn = React.createClass({      },      render: function () {          var flow = this.props.flow; -        var ssl = (flow.request.scheme == "https"); +        var ssl = (flow.request.scheme === "https");          var classes;          if (ssl) {              classes = "col-tls col-tls-https"; @@ -44,7 +44,7 @@ var IconColumn = React.createClass({              var contentType = ResponseUtils.getContentType(flow.response);              //TODO: We should assign a type to the flow somewhere else. -            if (flow.response.code == 304) { +            if (flow.response.code === 304) {                  icon = "resource-icon-not-modified";              } else if (300 <= flow.response.code && flow.response.code < 400) {                  icon = "resource-icon-redirect"; diff --git a/web/src/js/components/flowview/contentview.js b/web/src/js/components/flowview/contentview.js index 828c6d08..63d22c1c 100644 --- a/web/src/js/components/flowview/contentview.js +++ b/web/src/js/components/flowview/contentview.js @@ -27,7 +27,7 @@ var RawMixin = {          }      },      requestContent: function (nextProps) { -        if(this.state.request){ +        if (this.state.request) {              this.state.request.abort();          }          var request = MessageUtils.getContent(nextProps.flow, nextProps.message); @@ -38,11 +38,11 @@ var RawMixin = {          request.done(function (data) {              this.setState({content: data});          }.bind(this)).fail(function (jqXHR, textStatus, errorThrown) { -            if(textStatus === "abort"){ +            if (textStatus === "abort") {                  return;              }              this.setState({content: "AJAX Error: " + textStatus + "\r\n" + errorThrown}); -        }.bind(this)).always(function(){ +        }.bind(this)).always(function () {              this.setState({request: undefined});          }.bind(this)); @@ -55,8 +55,8 @@ var RawMixin = {              this.requestContent(nextProps);          }      }, -    componentWillUnmount: function(){ -        if(this.state.request){ +    componentWillUnmount: function () { +        if (this.state.request) {              this.state.request.abort();          }      }, @@ -94,7 +94,7 @@ var ViewJSON = React.createClass({          var json = this.state.content;          try {              json = JSON.stringify(JSON.parse(json), null, 2); -        } catch(e) { +        } catch (e) {          }          return <pre>{json}</pre>;      } @@ -139,7 +139,7 @@ var ContentMissing = React.createClass({  var TooLarge = React.createClass({      statics: { -        isTooLarge: function(message){ +        isTooLarge: function (message) {              var max_mb = ViewImage.matches(message) ? 10 : 0.2;              return message.contentLength > 1024 * 1024 * max_mb;          } @@ -225,8 +225,10 @@ var ContentView = React.createClass({              <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> +              +                <a className="btn btn-default btn-xs" href={downloadUrl}> +                    <i className="fa fa-download"/> +                </a>              </div>          </div>;      } diff --git a/web/src/js/components/flowview/messages.js b/web/src/js/components/flowview/messages.js index fe8fa200..cb8516fc 100644 --- a/web/src/js/components/flowview/messages.js +++ b/web/src/js/components/flowview/messages.js @@ -1,5 +1,8 @@  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"); @@ -24,20 +27,217 @@ var Headers = React.createClass({      }  }); -var Request = React.createClass({ +var InlineInput = React.createClass({ +    mixins: [common.ChildFocus], +    getInitialState: function () { +        return { +            editable: false +        }; +    }, +    render: function () { +        var Tag = this.props.tag || "span"; +        var className = "inline-input " + (this.props.className || ""); +        var html = {__html: _.escape(this.props.content)}; +        return <Tag +            {...this.props} +            tabIndex="0" +            className={className} +            contentEditable={this.state.editable || undefined} +            onInput={this.onInput} +            onFocus={this.onFocus} +            onBlur={this.onBlur} +            onKeyDown={this.onKeyDown} +            dangerouslySetInnerHTML={html} +        />; +    }, +    onKeyDown: function (e) { +        e.stopPropagation(); +        switch (e.keyCode) { +            case utils.Key.ESC: +                this.blur(); +                break; +            case utils.Key.ENTER: +                e.preventDefault(); +                if (!e.ctrlKey) { +                    this.blur(); +                } else { +                    this.props.onDone && this.props.onDone(); +                } +                break; +            default: +                break; +        } +    }, +    blur: function(){ +        this.getDOMNode().blur(); +        this.context.returnFocus && this.context.returnFocus(); +    }, +    selectContents: function () { +        var range = document.createRange(); +        range.selectNodeContents(this.getDOMNode()); +        var sel = window.getSelection(); +        sel.removeAllRanges(); +        sel.addRange(range); +    }, +    onFocus: function () { +        this.setState({editable: true}, this.selectContents); +    }, +    onBlur: function (e) { +        this.setState({editable: false}); +        this.handleChange(); +        this.props.onDone && this.props.onDone(); +    }, +    onInput: function () { +        this.handleChange(); +    }, +    handleChange: function () { +        var content = this.getDOMNode().textContent; +        if (content !== this.props.content) { +            this.props.onChange(content); +        } +    } +}); + +var ValidateInlineInput = React.createClass({ +    getInitialState: function () { +        return { +            content: ""+this.props.content, +            originalContent: ""+this.props.content +        }; +    }, +    onChange: function (val) { +        this.setState({ +            content: val +        }); +    }, +    onDone: function () { +        if (this.state.content === this.state.originalContent) { +            return true; +        } +        if (this.props.isValid(this.state.content)) { +            this.props.onChange(this.state.content); +        } else { +            this.setState({ +                content: this.state.originalContent +            }); +        } +    }, +    componentWillReceiveProps: function (nextProps) { +        if (nextProps.content !== this.state.content) { +            this.setState({ +                content: ""+nextProps.content, +                originalContent: ""+nextProps.content +            }) +        } +    }, +    render: function () { +        var className = this.props.className || ""; +        if (this.props.isValid(this.state.content)) { +            className += " has-success"; +        } else { +            className += " has-warning" +        } +        return <InlineInput {...this.props} +            className={className} +            content={this.state.content} +            onChange={this.onChange} +            onDone={this.onDone} +        />; +    } +}); + +var RequestLine = React.createClass({      render: function () {          var flow = this.props.flow; -        var first_line = [ -            flow.request.method, -            flowutils.RequestUtils.pretty_url(flow.request), -            "HTTP/" + flow.request.httpversion.join(".") -        ].join(" "); +        var url = flowutils.RequestUtils.pretty_url(flow.request); +        var httpver = "HTTP/" + flow.request.httpversion.join("."); -        //TODO: Styling +        return <div className="first-line request-line"> +            <ValidateInlineInput content={flow.request.method} onChange={this.onMethodChange} isValid={this.isValidMethod}/> +          +            <ValidateInlineInput content={url} onChange={this.onUrlChange} isValid={this.isValidUrl} /> +          +            <ValidateInlineInput content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} /> +        </div> +    }, +    isValidMethod: function (method) { +        return true; +    }, +    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: {httpversion: ver}} +        ); +    } +}); +var ResponseLine = React.createClass({ +    render: function () { +        var flow = this.props.flow; +        var httpver = "HTTP/" + flow.response.httpversion.join("."); +        return <div className="first-line response-line"> +            <ValidateInlineInput content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} /> +          +            <ValidateInlineInput content={flow.response.code} onChange={this.onCodeChange} isValid={this.isValidCode} /> +          +            <ValidateInlineInput content={flow.response.msg} onChange={this.onMsgChange} isValid={this.isValidMsg} /> + +        </div>; +    }, +    isValidCode: function (code) { +        return /^\d+$/.test(code); +    }, +    isValidMsg: function () { +        return true; +    }, +    onHttpVersionChange: function (nextVer) { +        var ver = flowutils.parseHttpVersion(nextVer); +        actions.FlowActions.update( +            this.props.flow, +            {response: {httpversion: 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> -                <div className="first-line">{ first_line }</div> +            <section className="request"> +                <RequestLine flow={flow}/> +                {/*<ResponseLine flow={flow}/>*/}                  <Headers message={flow.request}/>                  <hr/>                  <ContentView flow={flow} message={flow.request}/> @@ -49,17 +249,10 @@ var Request = React.createClass({  var Response = React.createClass({      render: function () {          var flow = this.props.flow; -        var first_line = [ -            "HTTP/" + flow.response.httpversion.join("."), -            flow.response.code, -            flow.response.msg -        ].join(" "); - -        //TODO: Styling -          return ( -            <section> -                <div className="first-line">{ first_line }</div> +            <section className="response"> +                {/*<RequestLine flow={flow}/>*/} +                <ResponseLine flow={flow}/>                  <Headers message={flow.response}/>                  <hr/>                  <ContentView flow={flow} message={flow.response}/> diff --git a/web/src/js/components/mainview.js b/web/src/js/components/mainview.js index 81bf3b03..9790a69e 100644 --- a/web/src/js/components/mainview.js +++ b/web/src/js/components/mainview.js @@ -11,6 +11,15 @@ var FlowView = require("./flowview/index.js");  var MainView = React.createClass({      mixins: [common.Navigation, common.State], +    childContextTypes: { +         returnFocus: React.PropTypes.func.isRequired +    }, +    getChildContext: function() { +         return { returnFocus: this.returnFocus }; +    }, +    returnFocus: function(){ +        this.getDOMNode().focus(); +    },      getInitialState: function () {          return {              flows: [], diff --git a/web/src/js/flow/utils.js b/web/src/js/flow/utils.js index 29462a78..aa91e3b6 100644 --- a/web/src/js/flow/utils.js +++ b/web/src/js/flow/utils.js @@ -1,9 +1,17 @@  var _ = require("lodash");  var $ = require("jquery"); +var defaultPorts = { +    "http": 80, +    "https": 443 +}; +  var MessageUtils = {      getContentType: function (message) { -        return this.get_first_header(message, /^Content-Type$/i).split(";")[0].trim(); +        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. @@ -36,25 +44,20 @@ var MessageUtils = {          }          return false;      }, -    getContentURL: function(flow, message){ -        if(message === flow.request){ +    getContentURL: function (flow, message) { +        if (message === flow.request) {              message = "request"; -        } else if (message === flow.response){ +        } else if (message === flow.response) {              message = "response";          }          return "/flows/" + flow.id + "/" + message + "/content";      }, -    getContent: function(flow, message){ +    getContent: function (flow, message) {          var url = MessageUtils.getContentURL(flow, message);          return $.get(url);      }  }; -var defaultPorts = { -    "http": 80, -    "https": 443 -}; -  var RequestUtils = _.extend(MessageUtils, {      pretty_host: function (request) {          //FIXME: Add hostheader @@ -72,8 +75,53 @@ var RequestUtils = _.extend(MessageUtils, {  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); + +    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 +    MessageUtils: MessageUtils, +    parseUrl: parseUrl, +    parseHttpVersion: parseHttpVersion, +    isValidHttpVersion: isValidHttpVersion  };
\ No newline at end of file diff --git a/web/src/js/utils.js b/web/src/js/utils.js index be59db96..6fc11f9e 100644 --- a/web/src/js/utils.js +++ b/web/src/js/utils.js @@ -2,6 +2,10 @@ var $ = require("jquery");  var _ = require("lodash");  var actions = require("./actions.js"); +//debug +window.$ = $; +window._ = _; +  var Key = {      UP: 38,      DOWN: 40, @@ -28,17 +32,17 @@ 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){ +    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) +    if (bytes % Math.pow(1024, i) === 0)          precision = 0;      else          precision = 1; -    return (bytes/Math.pow(1024, i)).toFixed(precision) + prefix[i]; +    return (bytes / Math.pow(1024, i)).toFixed(precision) + prefix[i];  }; @@ -60,17 +64,16 @@ var formatTimeStamp = function (seconds) {      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){ +function reverseString(s) {      return String.fromCharCode.apply(String, -        _.map(s.split(""), function (c) { -            return 0xffff - c.charCodeAt(0); -        }) -    ) + end; +            _.map(s.split(""), function (c) { +                return 0xffff - c.charCodeAt(0); +            }) +        ) + end;  }  function getCookie(name) { @@ -82,21 +85,22 @@ 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.data) { -            options.data += ("&" + xsrf); +        if(options.url.indexOf("?") === -1){ +            options.url += "?" + xsrf;          } else { -            options.data = xsrf; +            options.url += "&" + xsrf;          }      }  });  // Log AJAX Errors  $(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) { -    if(thrownError === "abort"){ +    if (thrownError === "abort") {          return;      }      var message = jqXHR.responseText;      console.error(thrownError, message, arguments);      actions.EventLogActions.add_event(thrownError + ": " + message); +    alert(message);  });  module.exports = { @@ -104,5 +108,5 @@ module.exports = {      formatTimeDelta: formatTimeDelta,      formatTimeStamp: formatTimeStamp,      reverseString: reverseString, -    Key: Key +    Key: Key,  };
\ No newline at end of file  | 
