├── .babelrc ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── app.js ├── doc └── example.png ├── icon.png ├── index.html ├── index.js ├── openssl.cnf ├── package.json ├── phoenixmatrix.json ├── src ├── actions │ ├── config.js │ └── requests.js ├── app.js ├── components │ ├── Footer.jsx │ ├── Header.jsx │ ├── HttpHeaders.jsx │ ├── LeftSection.jsx │ ├── MainSection.jsx │ ├── PhoenixMatrixApp.jsx │ ├── Proxy.jsx │ ├── RequestBody.jsx │ ├── RequestDetail.jsx │ ├── RequestDetailHeader.jsx │ ├── RequestList.jsx │ ├── RequestListItem.jsx │ ├── ResponseDetail.jsx │ ├── Splitter.jsx │ └── VerticalButtonBar.jsx ├── constants │ ├── config-constants.js │ └── request-constants.js ├── dispatchers │ └── AppDispatcher.js ├── lib │ ├── angular.js │ ├── certificate.js │ ├── config.js │ ├── helpers.js │ ├── proxyFactory.js │ └── pure.js ├── main.js └── reducers │ ├── config.js │ ├── index.js │ └── requests.js ├── stylesheets ├── splitter.less └── style.less ├── vendor ├── bootstrap │ ├── css │ │ ├── bootstrap-theme.min.css │ │ └── bootstrap.min.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ ├── js │ │ └── bootstrap.min.js │ └── less │ │ ├── mixins.less │ │ ├── mixins │ │ ├── alerts.less │ │ ├── background-variant.less │ │ ├── border-radius.less │ │ ├── buttons.less │ │ ├── center-block.less │ │ ├── clearfix.less │ │ ├── forms.less │ │ ├── gradients.less │ │ ├── grid-framework.less │ │ ├── grid.less │ │ ├── hide-text.less │ │ ├── image.less │ │ ├── labels.less │ │ ├── list-group.less │ │ ├── nav-divider.less │ │ ├── nav-vertical-align.less │ │ ├── opacity.less │ │ ├── pagination.less │ │ ├── panels.less │ │ ├── progress-bar.less │ │ ├── reset-filter.less │ │ ├── resize.less │ │ ├── responsive-visibility.less │ │ ├── size.less │ │ ├── tab-focus.less │ │ ├── table-row.less │ │ ├── text-emphasis.less │ │ ├── text-overflow.less │ │ └── vendor-prefixes.less │ │ └── variables.less ├── font-awesome │ ├── css │ │ └── font-awesome.css │ └── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 └── jquery │ ├── jquery.min.js │ └── jquery.min.map └── webpack.config.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "react", "es2015-node5", "stage-1" ], 3 | "plugins": [ "transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", // https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb/.eslintrc 3 | "rules": { 4 | "max-len": [2, 117, 2], // Max length where things generally fit in a github code view width 5 | "comma-dangle": [2, "never"], // There's good reason to enforce comma dangling, but it throws people off. 6 | "strict": [2, "function"], // For now, we're not using modules, so strict has to be applied to a self executing function 7 | "no-extra-strict": 0, // Disabling legacy rule. Above rule overwrites it. 8 | "func-names": 0, // Most functions are named automatically in ES6. 9 | "no-console": 2, // the following rules are warnings in AirBNB. We make them errors. 10 | "no-undef": 2, 11 | "no-debugger": 2, 12 | "no-alert": 2, 13 | "space-in-parens": [2, "never"], // Enforce lack of spaces inside parens entirely for style reasons 14 | "object-curly-spacing": [2, "never"], // Enforce spaces inside curleys entirely for style reasons 15 | "arrow-spacing": 2, // Enforce spaces around arrow functions entirely for style reasons 16 | "no-constant-condition": 2, 17 | "eqeqeq": [2, "allow-null"], 18 | "no-eq-null": 0, 19 | "no-undef": 2, 20 | "strict": 0, 21 | "react/jsx-curly-spacing": [2, "never"], 22 | "no-unused-expressions": 0, 23 | "indent": [2, 2, {"SwitchCase": 0}] 24 | }, 25 | "ecmaFeatures": { 26 | "modules": true, 27 | "generators": true, 28 | "experimentalObjectRestSpread": true 29 | }, 30 | "globals": { 31 | "chai": true, 32 | "expect": true, 33 | "sinon": true 34 | }, 35 | "env": { 36 | "mocha": true, 37 | "worker": true, 38 | "jquery": true, 39 | "node": true, 40 | "commonjs": true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | certificate 4 | npm-debug.log 5 | dist 6 | git 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Francois Ward 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PhoenixMatrix web development proxy 2 | =================================== 3 | 4 | [![Join the chat at https://gitter.im/Phoenixmatrix/phoenixmatrix-proxy](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Phoenixmatrix/phoenixmatrix-proxy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | _v0.2.4 technical preview_ 7 | 8 | Web debugging proxy in the spirit of Fiddler and Charles Proxy, written in JavaScript with [Electron Shell](http://electron.atom.io/) 9 | and node. 10 | 11 | Tested on MacOSX El Captan, Windows 8.1/10 and Ubuntu 14. Expected to break often. 12 | 13 | ## Release notes 14 | 15 | **v0.2.5** 16 | * Migrated to Webpack and some cleanup. Turns out I'm so addicted to Webpack I can't avoid it even when I have CommonJS available. 17 | 18 | **v0.2.4** 19 | * Refactor to Redux, making future improvement easier to implement. 20 | * Still very messy from all the previous transitions, and not currently using Redux best practice: you probably don't want to copy this code :) 21 | 22 | **v0.2.3** 23 | * Updated some dependencies to work with the latest version of Electron 24 | * With this update, modern browsers shouldn't choke on the TLS version anymore. 25 | * Regression: A lot of deprecation errors to deal with, and the development mode toolbar now shows up. Next release should clean things up. 26 | 27 | **v0.2.2** 28 | * Some rendering performance improvements 29 | * No more OpenSSL dependency (using the fantastic [forge](https://github.com/digitalbazaar/forge)) 30 | * That means Windows users can now launch PhoenixMatrix from powershell, cmd.exe, whatever. It "just works" on all 3 major platforms. 31 | * Since the certificates are now generated in javascript synchronously, there's few performance blips the first time. This will be addressed in the future. 32 | 33 | **v0.2.1** 34 | * Migrated from Angular to React 35 | 36 | **v0.2.0** 37 | * Moved from nw.js to Electron Shell 38 | * Updated to the latest version of Babel 39 | * Updated Babel transformer blacklist for features iojs supports natively to clean up the generated code 40 | * Removed a bunch of hacks for cross platform support that are no longer necessary for Electron Shell. 41 | 42 | ## Warning: Technical Preview 43 | This is a technical preview at best!! 44 | 45 | The UI is far from finished, the code is a mess, there's a lot of bugs. Expect crashes, slowness, and generally an 46 | unfinished product feel. It 'works' when stars are aligned, and that's as much as you should expect. 47 | 48 | Again, the code is a mess. Please don't look at it and email me that its bad. For now my priority was to get things working. 49 | 50 | ### Features 51 | 52 | ![PhoenixMatrix web development proxy](/doc/example.png?raw=true "See what happens behind the scene") 53 | 54 | * Works with all major browsers 55 | * Works on all operating systems that Electron Shell (formerly Atom Shell) supports. Sorry Windows XP! 56 | * Supports both http and https 57 | * Allows real time https decryption with no scary warning (not even from Chrome!) 58 | * Handles gzipped responses (doesn't yet decode base64 encoded bodies though. That's coming) 59 | * Dynamically creates its own certificates for https decryption. No need to fiddle with OpenSSL! No OpenSSL dependencies whatsoever. 60 | * Secure! The certificate authority is created on the fly the first time you launch PhoenixMatrix. No one else will have the private key. 61 | * Free and open source the MIT license. Feel free to hack it up. 62 | * Built on open web technology. JavaScript, CSS, node.js (well, Electron Shell uses a fork of io.js. Close enough!). 63 | * Uses the latest EcmaScript 6 features, including generators, for [better asyncronous code](http://eng.localytics.com/better-asynchronous-javascript/) 64 | 65 | ### Setup 66 | * Clone this repo 67 | * Open a command prompt and navigate to the location where you cloned it. 68 | * Run `npm install`. Electron Shell will be downloaded here, so expect a big download. 69 | * Run `gulp` 70 | * You should now see the GUI application. Setup the proxy in your browser of choice (or in your application), and you're good to go! 71 | * From now on you can use `npm start` to run PhoenixMatrix without rebuilding everything. 72 | 73 | ### Configuring your browser to use the proxy 74 | 75 | Different browsers and operating systems use different methods of configuring proxies. For IE, Safari and Chrome in Windows and MacOSX, configure 76 | the proxy from the system settings/control panel. For FireFox, configure the proxy in the options -> advanced under network. The default port is 3002 but 77 | can be configured 78 | 79 | Exclude SSl/HTTPS from using the proxy if you don't need it and/or don't want to go through the steps to configure a certificate for man-in-the-middle decryption. 80 | 81 | For https support, after running PhoenixMatrix, look in the certificate folder where you cloned the repo. Import `ca.crt` in your browser (Firefox) 82 | or as a system certificate for most other browsers/operating systems. If prompted, the certificate only need to be used for websites. On Windows, the certificate needs to 83 | be configured in the Trusted Certificate Authorities (you can just double click the certificate to launch the wizard). You may need to restart PhoenixMatrix and/or your browser after doing this. 84 | 85 | PhoenixMatrix uses a certificate authority to generate individual server certificates, so only the one certificate needs to be installed. 86 | 87 | **if your pages aren't loading**: If after installing the certificate in your browser, pages aren't loading, close PhoenixMatrix andor your browser and restart it/them. Seems like the invalid certificate 88 | behavior of some browsers leave the proxy in a weird state 89 | 90 | ### Configuration 91 | 92 | The available options can be found in phoenixmatrix.json, in JSON format (duh!). 93 | * `includeConnect`: if you want to see http CONNECT requests, which will happen whenever you try to make an https request while using the proxy. 94 | * `proxyPort`: used to configure which port the proxy listens to. Make sure to use a free port. Default to 3002. 95 | * `httpsProxyPort`: used to configure on which port the https proxy listens to. This is only used internally and will not work 96 | if you point your browser to it directly. 97 | * `certificateExpiration`: how many days should the generated certificates be valid for 98 | 99 | ### Hacking and debugging PhoenixMatrix 100 | 101 | PhoenixMatrix is built on top of Electron Shell, which in turn is built on top of Chromium and iojs. 102 | This means you can easily add features or hack it up the same way you would a web page built with React and node/iojs. 103 | 104 | Like the Chrome browser, Electron Shell supports the devtools you know: with the window focused, just hit ctrl+shift+i (command+shift+i on mac)! 105 | 106 | ### Caveats 107 | * On MacOSX, when using the trackpad, scrolling isn't as smooth as it should be. 108 | * As of the technical preview, it is not possible to directly disable https support. If you don't want to setup the certificate, ensure your browser is not 109 | configured to use the proxy for HTTPS if you do not want it to stop you from browsing https sites during development. 110 | * The proxy currently only checks individual server certificate expiration dates to regenerate them. If you get an error that the CA certificate isn't valid 111 | even though you imported it, just delete the content of the certificate folder, restart PhoenixMatrix and import ca.crt again 112 | * Expect the code to change a LOT from now on. This was hacked up quickly to get things working. If you make a fork, don't expect to easily be able to merge from 113 | upstream for very long. 114 | * Doesn't support old HTTP versions, web sockets, HTTP/2, old servers or uncommon network setups. 115 | 116 | ### Thanks, inspiration and resources 117 | 118 | Inspiration taken from the following projects (I don't use the, but I learnt a lot from the source). Thanks! : 119 | * [node-http-proxy](https://github.com/nodejitsu/node-http-proxy) 120 | * [pem](https://github.com/andris9/pem) 121 | 122 | Splitter was ported to React from [bg-splitter](https://github.com/blackgate/bg-splitter). Thanks! 123 | My port isn't polished enough to warrant its own repo, but if anyone is interested, I can do so. 124 | 125 | ### License 126 | 127 | The MIT License (MIT) 128 | 129 | Copyright (c) 2015 Francois Ward 130 | 131 | Permission is hereby granted, free of charge, to any person obtaining a copy 132 | of this software and associated documentation files (the "Software"), to deal 133 | in the Software without restriction, including without limitation the rights 134 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 135 | copies of the Software, and to permit persons to whom the Software is 136 | furnished to do so, subject to the following conditions: 137 | 138 | The above copyright notice and this permission notice shall be included in all 139 | copies or substantial portions of the Software. 140 | 141 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 142 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 143 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 144 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 145 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 146 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 147 | SOFTWARE. 148 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // This will eventually become a command line version of the proxy. For now though, it's very broken. 2 | 3 | var proxyFactory = require('./src/lib/proxy'); 4 | 5 | var onRequest = function(data) { 6 | console.log('request: ' + data.method + ' ' + data.url); 7 | }; 8 | 9 | var onResponse = function(data) { 10 | console.log('response: ' + data.method + ' ' + data.url); 11 | }; 12 | 13 | var httpProxy; 14 | proxyFactory.createHttpProxy({port: 3002}).then(function(proxy) { 15 | httpProxy = proxy; 16 | proxy.on('proxyRequest', onRequest); 17 | proxy.on('proxyResponse', onResponse); 18 | proxy.on('proxyConnect', function(data) { 19 | console.log('connect request: ' + data.state + ' ' + data.url + ':' + data.port); 20 | }); 21 | return proxyFactory.createHttpsProxy({port: 8443}); 22 | }) 23 | .then(function(httpsProxy) { 24 | httpsProxy.on('proxyRequest', onRequest); 25 | httpsProxy.on('proxyResponse', onResponse); 26 | httpProxy.enableHttpsProxy(httpsProxy); 27 | }); 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /doc/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenixmatrix/phoenixmatrix-proxy/0edb2448571e4627a1c51bf571998e2db86963dc/doc/example.png -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenixmatrix/phoenixmatrix-proxy/0edb2448571e4627a1c51bf571998e2db86963dc/icon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PhoenixMatrix Proxy 5 | 8 | 9 | 10 | 11 | 12 |
13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./src/main'); 2 | -------------------------------------------------------------------------------- /openssl.cnf: -------------------------------------------------------------------------------- 1 | distinguished_name = req_distinguished_name 2 | [req_distinguished_name] 3 | [v3_req] 4 | [v3_ca] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phoenixmatrix", 3 | "version": "0.2.5", 4 | "description": "A cross platform proxy for web developer that supports https", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "electron .", 8 | "build": "webpack --progress -c", 9 | "build:hot": "webpack --progress -c -w" 10 | }, 11 | "author": "Francois Ward", 12 | "license": "MIT", 13 | "dependencies": { 14 | "babel": "^6.3.26", 15 | "babel-core": "^6.4.0", 16 | "babel-loader": "^6.2.1", 17 | "babel-plugin-transform-es2015-modules-commonjs": "^6.4.0", 18 | "babel-plugin-transform-runtime": "^6.3.13", 19 | "babel-polyfill": "^6.3.14", 20 | "babel-preset-es2015-node5": "^1.1.1", 21 | "babel-preset-react": "^6.3.13", 22 | "babel-preset-stage-1": "^6.3.13", 23 | "babel-runtime": "^6.3.19", 24 | "bluebird": "=2.3.11", 25 | "classnames": "^2.1.1", 26 | "connect": "~3.1.1", 27 | "css-loader": "^0.22.0", 28 | "electron-prebuilt": "^0.36.2", 29 | "file-loader": "^0.8.5", 30 | "imports-loader": "^0.6.5", 31 | "json-loader": "^0.5.4", 32 | "keymirror": "^0.1.1", 33 | "less": "^2.6.0", 34 | "less-loader": "^2.2.2", 35 | "lodash": "^3.10.1", 36 | "mkdirp": "^0.5.1", 37 | "moment": "^2.10.2", 38 | "node-forge": "^0.6.26", 39 | "node-uuid": "^1.4.7", 40 | "react": "^0.14.5", 41 | "react-addons-shallow-compare": "^0.14.5", 42 | "react-bootstrap": "^0.21.2", 43 | "react-dom": "^0.14.5", 44 | "react-redux": "^4.0.6", 45 | "redux": "^3.0.5", 46 | "redux-thunk": "^1.0.3", 47 | "run-sequence": "^1.0.2", 48 | "style-loader": "^0.13.0", 49 | "url-loader": "^0.5.7", 50 | "webpack": "^1.12.10" 51 | }, 52 | "devDependencies": { 53 | "eslint": "^1.10.3", 54 | "eslint-config-airbnb": "^2.1.1", 55 | "eslint-plugin-react": "^3.13.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /phoenixmatrix.json: -------------------------------------------------------------------------------- 1 | { 2 | "includeConnect": false, 3 | "proxyPort": 3002, 4 | "httpsProxyPort": 8443, 5 | "certificateExpiration": 364 6 | } 7 | -------------------------------------------------------------------------------- /src/actions/config.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import config from '../lib/config'; 3 | 4 | export const TOGGLE_CONNECT = 'TOGGLE_CONNECT'; 5 | export const LOAD_CONFIG = 'LOAD_CONFIG'; 6 | 7 | export const loadConfig = () => { 8 | return {type: LOAD_CONFIG, config: _.cloneDeep(config)}; 9 | }; 10 | 11 | export const toggleConnect = () => { 12 | config.includeConnect = !config.includeConnect; 13 | return {type: TOGGLE_CONNECT, includeConnect: config.includeConnect}; 14 | }; 15 | -------------------------------------------------------------------------------- /src/actions/requests.js: -------------------------------------------------------------------------------- 1 | export const SELECT_REQUEST = 'SELECT_REQUEST'; 2 | export const UPDATE_REQUEST = 'APPEND_REQUEST'; 3 | export const SET_FILTER = 'SET_FILTER'; 4 | export const CLEAR = 'CLEAR'; 5 | export const TOGGLE_PAUSE = 'TOGGLE_PAUSE'; 6 | 7 | export const selectRequest = request => ({type: SELECT_REQUEST, request}); 8 | export const updateRequest = request => ({type: UPDATE_REQUEST, request}); 9 | export const setFilter = expression => { 10 | const filter = expression ? expression.trim() : ''; 11 | return {type: SET_FILTER, filter}; 12 | }; 13 | export const clear = () => ({type: CLEAR}); 14 | export const togglePause = () => ({type: TOGGLE_PAUSE}); 15 | 16 | export const pushRequest = (data) => { 17 | return (dispatch, getState) => { 18 | const {config: {paused}} = getState(); 19 | if (!paused && data.id) { 20 | dispatch(updateRequest(data)); 21 | } 22 | }; 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {createStore, applyMiddleware} from 'redux'; 4 | import {Provider} from 'react-redux'; 5 | import thunk from 'redux-thunk'; 6 | 7 | import PhoenixMatrixApp from './components/PhoenixMatrixApp'; 8 | import reducer from './reducers'; 9 | 10 | const createStoreWithMiddleware = applyMiddleware( 11 | thunk 12 | )(createStore); 13 | const store = createStoreWithMiddleware(reducer); 14 | 15 | ReactDOM.render( 16 | 17 | 18 | , 19 | document.getElementById('main') 20 | ); 21 | -------------------------------------------------------------------------------- /src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import pure from '../lib/pure'; 3 | 4 | export default class Footer extends React.Component { 5 | render() { 6 | return ( 7 |
8 | ); 9 | } 10 | } 11 | pure(Footer); 12 | -------------------------------------------------------------------------------- /src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import React from 'react'; 4 | import pure from '../lib/pure'; 5 | 6 | export default class Header extends React.Component { 7 | componentWillMount() { 8 | const {onFilterChange} = this.props; 9 | this.onFilterChangeDebounced = _.debounce(onFilterChange, 500); 10 | } 11 | 12 | onChange(e) { 13 | this.onFilterChangeDebounced(e.target.value); 14 | } 15 | 16 | render() { 17 | return ( 18 |
19 | PhoenixMatrix web debugging proxy v0.1 Preview 20 |
21 | this.onChange(e)} 27 | /> 28 |
29 |
30 | ); 31 | } 32 | } 33 | 34 | Header.propTypes = { 35 | onFilterChange: React.PropTypes.func 36 | }; 37 | 38 | pure(Header); 39 | -------------------------------------------------------------------------------- /src/components/HttpHeaders.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import pure from '../lib/pure'; 3 | import _ from 'lodash'; 4 | 5 | export default class HttpHeaders extends React.Component { 6 | constructor() { 7 | super(); 8 | this.state = {showTable: true}; 9 | } 10 | 11 | toggle() { 12 | this.setState({showTable: !this.state.showTable}); 13 | } 14 | 15 | render() { 16 | const headers = this.props.headers; 17 | 18 | let content = null; 19 | let headerTable; 20 | 21 | if (headers && Object.keys(headers).length) { 22 | if (this.state.showTable) { 23 | headerTable = ( 24 | 25 | 26 | { 27 | _.map(headers, (value, key) => { 28 | return ( 29 | 30 | 31 | 32 | 33 | ); 34 | }) 35 | } 36 | 37 |
{key}{value}
38 | ); 39 | } 40 | 41 | content = ( 42 |
43 |
this.toggle()}> 44 | Headers 45 |
46 | {headerTable} 47 |
48 | ); 49 | } 50 | 51 | return content; 52 | } 53 | } 54 | 55 | HttpHeaders.propTypes = { 56 | headers: React.PropTypes.object 57 | }; 58 | 59 | pure(HttpHeaders); 60 | -------------------------------------------------------------------------------- /src/components/LeftSection.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import pure from '../lib/pure'; 3 | import VerticalButtonBar from './VerticalButtonBar'; 4 | import RequestList from './RequestList'; 5 | 6 | export default class LeftSection extends React.Component { 7 | render() { 8 | const { 9 | onSelectRequest, 10 | onClear, 11 | onToggleConnect, 12 | onTogglePause, 13 | paused, 14 | includeConnect, 15 | requests, 16 | selectedRequest 17 | } = this.props; 18 | 19 | return ( 20 |
21 | 28 | 33 |
34 | ); 35 | } 36 | } 37 | 38 | LeftSection.propTypes = { 39 | paused: React.PropTypes.bool, 40 | includeConnect: React.PropTypes.bool, 41 | requests: React.PropTypes.array, 42 | selectedRequest: React.PropTypes.object, 43 | onSelectRequest: React.PropTypes.func, 44 | onClear: React.PropTypes.func, 45 | onToggleConnect: React.PropTypes.func, 46 | onTogglePause: React.PropTypes.func 47 | }; 48 | 49 | 50 | pure(LeftSection); 51 | -------------------------------------------------------------------------------- /src/components/MainSection.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import pure from '../lib/pure'; 3 | import RequestDetail from './RequestDetail'; 4 | import ResponseDetail from './ResponseDetail'; 5 | 6 | export default class MainSection extends React.Component { 7 | render() { 8 | const request = this.props.selectedRequest; 9 | 10 | let requestDetail; 11 | let responseDetail; 12 | 13 | if (request) { 14 | requestDetail = ; 15 | responseDetail = ; 16 | } 17 | 18 | return ( 19 |
20 | {requestDetail} 21 | {responseDetail} 22 |
23 | ); 24 | } 25 | } 26 | 27 | MainSection.propTypes = { 28 | selectedRequest: React.PropTypes.object 29 | }; 30 | 31 | pure(MainSection); 32 | -------------------------------------------------------------------------------- /src/components/PhoenixMatrixApp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | 4 | import Proxy from './Proxy'; 5 | import Header from './Header'; 6 | import LeftSection from './LeftSection'; 7 | import MainSection from './MainSection'; 8 | import Footer from './Footer'; 9 | import Splitter from './Splitter'; 10 | 11 | import {loadConfig, toggleConnect} from '../actions/config'; 12 | import {setFilter, selectRequest, pushRequest, togglePause, clear} from '../actions/requests'; 13 | 14 | import '../../vendor/bootstrap/css/bootstrap.min'; 15 | import '../../vendor/font-awesome/css/font-awesome'; 16 | import '../../stylesheets/splitter'; 17 | import '../../stylesheets/style'; 18 | 19 | class PhoenixMatrixApp extends React.Component { 20 | componentWillMount() { 21 | const {onLoadConfig} = this.props; 22 | onLoadConfig(); 23 | } 24 | 25 | render() { 26 | const { 27 | onProxyEvent, 28 | onSelectRequest, 29 | onClear, 30 | onToggleConnect, 31 | onTogglePause, 32 | onFilterChange, 33 | proxyPort, 34 | httpsProxyPort, 35 | requests, 36 | selectedRequest, 37 | paused, 38 | includeConnect, 39 | configLoaded 40 | } = this.props; 41 | 42 | let proxy = null; 43 | if (configLoaded) { 44 | proxy = ; 45 | } 46 | 47 | return ( 48 |
49 | {proxy} 50 |
51 |
52 | 53 |
54 | 64 |
65 |
66 | 67 |
68 |
69 |
70 |
71 |
72 | ); 73 | } 74 | } 75 | 76 | PhoenixMatrixApp.propTypes = { 77 | configLoaded: React.PropTypes.bool, 78 | proxyPort: React.PropTypes.number, 79 | httpsProxyPort: React.PropTypes.number, 80 | requests: React.PropTypes.array, 81 | selectedRequest: React.PropTypes.object, 82 | paused: React.PropTypes.bool, 83 | includeConnect: React.PropTypes.bool, 84 | onLoadConfig: React.PropTypes.func, 85 | onProxyEvent: React.PropTypes.func, 86 | onSelectRequest: React.PropTypes.func, 87 | onClear: React.PropTypes.func, 88 | onToggleConnect: React.PropTypes.func, 89 | onTogglePause: React.PropTypes.func, 90 | onFilterChange: React.PropTypes.func 91 | }; 92 | 93 | function mapStateToProps(state) { 94 | const requests = state.requests.filteredRequests.map(id => state.requests.requestsMap[id]); 95 | return { 96 | proxyPort: state.config.proxyPort, 97 | httpsProxyPort: state.config.httpsProxyPort, 98 | requests, 99 | selectedRequest: state.requests.selectedRequest, 100 | paused: state.requests.paused, 101 | includeConnect: state.config.includeConnect, 102 | configLoaded: state.config.configLoaded 103 | }; 104 | } 105 | 106 | function mapDispatchToProps(dispatch) { 107 | return { 108 | onProxyEvent: (request) => { 109 | dispatch(pushRequest(request)); 110 | }, 111 | onLoadConfig: () => dispatch(loadConfig()), 112 | onSelectRequest: request => dispatch(selectRequest(request)), 113 | onClear: () => dispatch(clear()), 114 | onToggleConnect: () => dispatch(toggleConnect()), 115 | onTogglePause: () => dispatch(togglePause()), 116 | onFilterChange: (filter) => dispatch(setFilter(filter)) 117 | }; 118 | } 119 | 120 | export default connect( 121 | mapStateToProps, 122 | mapDispatchToProps 123 | )(PhoenixMatrixApp); 124 | -------------------------------------------------------------------------------- /src/components/Proxy.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {coroutine as async} from 'bluebird'; 3 | import proxyFactory from '../lib/proxyFactory'; 4 | 5 | const initProxy = async(function* (onProxyEvent, proxyPort, httpsProxyPort) { 6 | const httpProxy = yield proxyFactory.createHttpProxy({port: proxyPort}); 7 | 8 | httpProxy.on('proxyRequest', onProxyEvent); 9 | httpProxy.on('proxyResponse', onProxyEvent); 10 | httpProxy.on('proxyConnect', onProxyEvent); 11 | const httpsProxy = yield proxyFactory.createHttpsProxy({port: httpsProxyPort}); 12 | 13 | httpsProxy.on('proxyRequest', onProxyEvent); 14 | httpsProxy.on('proxyResponse', onProxyEvent); 15 | httpProxy.enableHttpsProxy(httpsProxy); 16 | }); 17 | 18 | export default class Proxy extends React.Component { 19 | componentWillMount() { 20 | const {onProxyEvent, proxyPort, httpsProxyPort} = this.props; 21 | initProxy(onProxyEvent, proxyPort, httpsProxyPort); 22 | } 23 | 24 | render() { 25 | return false; 26 | } 27 | } 28 | 29 | Proxy.propTypes = { 30 | onProxyEvent: React.PropTypes.func, 31 | proxyPort: React.PropTypes.number, 32 | httpsProxyPort: React.PropTypes.number 33 | }; 34 | 35 | export default Proxy; 36 | -------------------------------------------------------------------------------- /src/components/RequestBody.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import pure from '../lib/pure'; 3 | 4 | export default class RequestBody extends React.Component { 5 | constructor() { 6 | super(); 7 | this.state = {showBody: false}; 8 | } 9 | 10 | toggle() { 11 | this.setState({showBody: !this.state.showBody}); 12 | } 13 | 14 | render() { 15 | const body = this.props.body; 16 | let content = null; 17 | if (body && body.length) { 18 | let contentBody; 19 | if (this.state.showBody) { 20 | contentBody =
{body}
; 21 | } 22 | 23 | content = ( 24 |
25 |
this.toggle()}>Body
26 | {contentBody} 27 |
28 | ); 29 | } 30 | 31 | return content; 32 | } 33 | } 34 | 35 | RequestBody.propTypes = { 36 | body: React.PropTypes.string 37 | }; 38 | 39 | pure(RequestBody); 40 | -------------------------------------------------------------------------------- /src/components/RequestDetail.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import pure from '../lib/pure'; 3 | import RequestDetailHeader from './RequestDetailHeader'; 4 | import HttpHeaders from './HttpHeaders'; 5 | import RequestBody from './RequestBody'; 6 | 7 | export default class RequestDetail extends React.Component { 8 | render() { 9 | const request = this.props.request; 10 | 11 | return ( 12 |
13 | 14 |
15 |
16 |

Request

17 |
18 |
19 | 20 | 21 |
22 |
23 |
24 | ); 25 | } 26 | } 27 | 28 | RequestDetail.propTypes = { 29 | request: React.PropTypes.object 30 | }; 31 | 32 | pure(RequestDetail); 33 | -------------------------------------------------------------------------------- /src/components/RequestDetailHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import pure from '../lib/pure'; 4 | 5 | export default class RequestDetailHeader extends React.Component { 6 | 7 | render() { 8 | const request = this.props.request; 9 | 10 | const classes = classNames({ 11 | 'request-header': true, 12 | 'well': (!request.statusCode || request.statusCode < 400) && !request.error, 13 | 'alert alert-danger': request.error || request.statusCode >= 400 14 | }); 15 | 16 | return ( 17 |
18 |
19 | {request.statusCode} 20 | {request.method} 21 | 22 | {(request.isSSL ? 'https://' : 'http://') + request.host} 23 | {request.port ? (':' + request.port) : ''} 24 |
25 |
26 | {request.path} 27 |
28 |
29 | ); 30 | } 31 | } 32 | 33 | RequestDetailHeader.propTypes = { 34 | request: React.PropTypes.object 35 | }; 36 | 37 | pure(RequestDetailHeader); 38 | -------------------------------------------------------------------------------- /src/components/RequestList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import pure from '../lib/pure'; 3 | import RequestListItem from './RequestListItem'; 4 | 5 | const ITEM_HEIGHT = 50; 6 | const BUFFER_SIZE = 100; 7 | 8 | export default class RequestList extends React.Component { 9 | componentDidMount() { 10 | this.attachScrollListener(); 11 | } 12 | 13 | componentWillUpdate() { 14 | const node = this.refs.scrollingList; 15 | this.scrollPosition = node.scrollTop; 16 | this.height = node.offsetHeight; 17 | this.shouldScrollBottom = this.scrollPosition + this.height >= node.scrollHeight; 18 | } 19 | 20 | componentDidUpdate() { 21 | const node = this.refs.scrollingList; 22 | const virtualContainer = this.refs.virtualContainer; 23 | const list = this.refs.requestList; 24 | 25 | const count = this.props.requests.length; 26 | virtualContainer.style.height = count * ITEM_HEIGHT + 'px'; 27 | list.style.top = this.start * ITEM_HEIGHT + 'px'; 28 | 29 | if (this.shouldScrollBottom) { 30 | node.scrollTop = node.scrollHeight; 31 | } 32 | } 33 | 34 | componentWillUnmount() { 35 | this.detachScrollListener(); 36 | } 37 | 38 | onScroll() { 39 | this.forceUpdate(); 40 | } 41 | 42 | detachScrollListener() { 43 | const node = this.refs.scrollingList; 44 | node.removeEventListener('scroll', () => this.onScroll()); 45 | } 46 | 47 | attachScrollListener() { 48 | const node = this.refs.scrollingList; 49 | node.addEventListener('scroll', () => this.onScroll()); 50 | } 51 | 52 | render() { 53 | const {onSelectRequest, requests, selectedRequest} = this.props; 54 | const listItems = []; 55 | const scrollY = this.scrollPosition; 56 | const start = Math.max((scrollY / ITEM_HEIGHT - BUFFER_SIZE | 0), 0); 57 | const end = Math.min(start + (this.height / ITEM_HEIGHT + (BUFFER_SIZE * 2) | 0), requests.length); 58 | 59 | this.start = start; 60 | requests.slice(start, end).forEach(function (request) { 61 | listItems.push(); 67 | }, this); 68 | 69 | return ( 70 |
71 |
72 |
73 |
    74 | {listItems} 75 |
76 |
77 |
78 |
79 | ); 80 | } 81 | } 82 | 83 | RequestList.propTypes = { 84 | requests: React.PropTypes.array, 85 | selectedRequest: React.PropTypes.object, 86 | onSelectRequest: React.PropTypes.func 87 | }; 88 | 89 | pure(RequestList); 90 | -------------------------------------------------------------------------------- /src/components/RequestListItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import pure from '../lib/pure'; 4 | 5 | const ITEM_HEIGHT = 50; 6 | 7 | export default class RequestListItem extends React.Component { 8 | render() { 9 | const {onSelectRequest, request} = this.props; 10 | const classes = classNames({ 11 | 'request-item': true, 12 | 'selected-request': this.props.selected, 13 | 'pending-request': request.state !== 'response' && request.method !== 'CONNECT', 14 | 'error-request': request.statusCode >= 400 || request.error, 15 | 'cached-request': request.statusCode === 304 16 | }); 17 | 18 | const style = {height: ITEM_HEIGHT + 'px'}; 19 | 20 | return ( 21 |
  • onSelectRequest(request)}> 22 |
    23 | 24 | {request.method === 'CONNECT' ? request.method : request.statusCode} 25 | 26 | {request.method === 'CONNECT' ? '' : request.method} 27 | {request.isSSL ? 'https' : 'http'} 28 | {request.host + (request.port ? ':' + request.port : '')} 29 |
    30 |
    {request.path}
    31 |
  • 32 | ); 33 | } 34 | } 35 | 36 | RequestListItem.propTypes = { 37 | request: React.PropTypes.object, 38 | selected: React.PropTypes.bool, 39 | onSelectRequest: React.PropTypes.func 40 | }; 41 | 42 | pure(RequestListItem); 43 | -------------------------------------------------------------------------------- /src/components/ResponseDetail.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import pure from '../lib/pure'; 3 | import HttpHeaders from './HttpHeaders'; 4 | import RequestBody from './RequestBody'; 5 | 6 | export default class ResponseDetail extends React.Component { 7 | render() { 8 | const request = this.props.request; 9 | 10 | return ( 11 |
    12 |
    13 |
    14 |

    Response

    15 |
    16 |
    17 | 18 | 19 |
    20 |
    21 |
    22 | ); 23 | } 24 | } 25 | 26 | ResponseDetail.propTypes = { 27 | request: React.PropTypes.object 28 | }; 29 | 30 | pure(ResponseDetail); 31 | 32 | -------------------------------------------------------------------------------- /src/components/Splitter.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import classNames from 'classnames'; 4 | import pure from '../lib/pure'; 5 | 6 | export default class Splitter extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = {dragging: false, handlerPos: props.initialPosition || 100}; 10 | } 11 | 12 | componentDidMount() { 13 | document.addEventListener('mouseup', () => this.onMouseUp()); 14 | document.addEventListener('mousemove', e => this.onMouseMove(e)); 15 | } 16 | 17 | componentWillUnmount() { 18 | document.removeEventListener('mouseup', () => this.onMouseUp()); 19 | document.removeEventListener('mousemove', e => this.onMouseMove(e)); 20 | } 21 | 22 | onMouseUp() { 23 | this.setState({dragging: false}); 24 | } 25 | 26 | onMouseMove(e) { 27 | if (!this.state.dragging) { 28 | return; 29 | } 30 | 31 | const bounds = ReactDOM.findDOMNode(this).getBoundingClientRect(); 32 | const pane1min = this.refs.pane1.minSize; 33 | const pane2min = this.refs.pane2.minSize; 34 | 35 | let pos = 0; 36 | 37 | if (this.props.orientation === 'vertical') { 38 | const height = bounds.bottom - bounds.top; 39 | pos = e.clientY - bounds.top; 40 | 41 | if (pos < pane1min) { 42 | return; 43 | } 44 | 45 | if (height - pos < pane2min) { 46 | return; 47 | } 48 | 49 | this.setState({handlerPos: pos}); 50 | } else { 51 | const width = bounds.right - bounds.left; 52 | pos = e.clientX - bounds.left; 53 | 54 | if (pos < pane1min) { 55 | return; 56 | } 57 | if (width - pos < pane2min) { 58 | return; 59 | } 60 | 61 | this.setState({handlerPos: pos}); 62 | } 63 | } 64 | 65 | onMouseDown(e) { 66 | e.preventDefault(); 67 | this.setState({dragging: true}); 68 | } 69 | 70 | render() { 71 | const horizontal = this.props.orientation === 'horizontal'; 72 | 73 | const classes = classNames({ 74 | 'split-panes': true, 75 | horizontal, 76 | vertical: !horizontal 77 | }); 78 | 79 | const children = this.props.children; 80 | const pane1 = children[0]; 81 | const pane2 = children[1]; 82 | 83 | const handlerStyles = {}; 84 | const pane1Styles = {}; 85 | const pane2Styles = {}; 86 | 87 | const pane1min = pane1.minSize || 0; 88 | const pane2min = pane2.minSize || 0; 89 | 90 | const positionStyle = this.state.handlerPos + 'px'; 91 | 92 | if (horizontal) { 93 | pane1Styles.width = pane2Styles.left = handlerStyles.left = positionStyle; 94 | } else { 95 | pane1Styles.height = pane2Styles.top = handlerStyles.top = positionStyle; 96 | } 97 | 98 | return ( 99 |
    100 |
    {pane1}
    101 |
    this.onMouseDown(e)}>
    102 |
    {pane2}
    103 |
    104 | ); 105 | } 106 | } 107 | 108 | Splitter.propTypes = { 109 | orientation: React.PropTypes.string, 110 | minSize: React.PropTypes.number, 111 | initialPosition: React.PropTypes.number, 112 | children: React.PropTypes.node 113 | }; 114 | 115 | 116 | pure(Splitter); 117 | -------------------------------------------------------------------------------- /src/components/VerticalButtonBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {OverlayTrigger, Tooltip} from 'react-bootstrap'; 3 | import classNames from 'classnames'; 4 | import pure from '../lib/pure'; 5 | 6 | export default class VerticalButtonBar extends React.Component { 7 | render() { 8 | const {onClear, onToggleConnect, onTogglePause, paused, includeConnect} = this.props; 9 | 10 | const pauseClasses = classNames({ 11 | 'fa': true, 12 | 'fa-pause': true, 13 | 'selected': paused 14 | }); 15 | 16 | const connectClasses = classNames({ 17 | 'fa': true, 18 | 'fa-server': true, 19 | 'selected': includeConnect 20 | }); 21 | 22 | return ( 23 |
      24 | Clear requests}> 25 |
    • onClear()} /> 26 | 27 | Pause capture}> 28 |
    • onTogglePause()} /> 29 | 30 | Display CONNECT requests}> 31 |
    • onToggleConnect()} /> 32 | 33 |
    34 | ); 35 | } 36 | } 37 | 38 | VerticalButtonBar.propTypes = { 39 | paused: React.PropTypes.bool, 40 | includeConnect: React.PropTypes.bool, 41 | onClear: React.PropTypes.func, 42 | onToggleConnect: React.PropTypes.func, 43 | onTogglePause: React.PropTypes.func 44 | }; 45 | 46 | pure(VerticalButtonBar); 47 | -------------------------------------------------------------------------------- /src/constants/config-constants.js: -------------------------------------------------------------------------------- 1 | export default { 2 | TOGGLE_CONNECT: Symbol() 3 | }; 4 | -------------------------------------------------------------------------------- /src/constants/request-constants.js: -------------------------------------------------------------------------------- 1 | export default { 2 | SELECT_REQUEST: Symbol(), 3 | SET_FILTER: Symbol(), 4 | CLEAR: Symbol(), 5 | TOGGLE_PAUSE: Symbol() 6 | }; 7 | -------------------------------------------------------------------------------- /src/dispatchers/AppDispatcher.js: -------------------------------------------------------------------------------- 1 | import { Dispatcher } from 'flux'; 2 | 3 | export default new Dispatcher(); 4 | -------------------------------------------------------------------------------- /src/lib/angular.js: -------------------------------------------------------------------------------- 1 | global.window.angular.module('phoenixmatrix', ['bgDirectives', 'ui.bootstrap']); 2 | export default global.window.angular; -------------------------------------------------------------------------------- /src/lib/certificate.js: -------------------------------------------------------------------------------- 1 | import fsOrig from 'fs'; 2 | import crypto from 'crypto'; 3 | import {pki, md} from 'node-forge'; 4 | import Promise from 'bluebird'; 5 | import moment from 'moment'; 6 | 7 | import mkdirpOrig from 'mkdirp'; 8 | import helpers from './helpers'; 9 | import config from './config'; 10 | 11 | const async = Promise.coroutine; 12 | 13 | const fs = Promise.promisifyAll(fsOrig); 14 | const mkdirp = Promise.promisify(mkdirpOrig); 15 | 16 | const directory = './certificate/'; 17 | const caCertPath = directory + 'ca.crt'; 18 | const caKeyPath = directory + 'ca.key'; 19 | 20 | const domainCertificates = {}; 21 | let ca; 22 | const getCA = () => ca; 23 | 24 | const getSerial = () => crypto.randomBytes(Math.ceil(16 / 2)).toString('hex').slice(0, 16).toUpperCase(); 25 | const keys = pki.rsa.generateKeyPair(2048); 26 | 27 | const createCertificateAuthority = async(function* () { 28 | const certificate = pki.createCertificate(); 29 | certificate.publicKey = keys.publicKey; 30 | certificate.serialNumber = getSerial(); 31 | certificate.validity.notBefore = new Date(); 32 | certificate.validity.notAfter = new Date(); 33 | certificate.validity.notAfter.setFullYear(certificate.validity.notBefore.getFullYear() + 1); 34 | const attrs = [{ 35 | name: 'commonName', 36 | value: 'phoenixmatrix_do_not_trust' 37 | }, { 38 | name: 'organizationName', 39 | value: 'do_not_trust_phoenixmatrix' 40 | }, { 41 | name: 'countryName', 42 | value: 'US' 43 | }]; 44 | 45 | certificate.setExtensions([{ 46 | name: 'basicConstraints', 47 | cA: true 48 | }]); 49 | 50 | certificate.setSubject(attrs); 51 | certificate.setIssuer(attrs); 52 | certificate.sign(keys.privateKey, md.sha256.create()); 53 | 54 | ca = pki.certificateToPem(certificate); 55 | 56 | const privateKey = pki.privateKeyToPem(keys.privateKey); 57 | yield fs.writeFileAsync(caKeyPath, privateKey); 58 | return yield fs.writeFileAsync(caCertPath, ca); 59 | }); 60 | 61 | let keyPairPaths = function(domain) { 62 | return { 63 | keyPath: `${directory}domains/${domain}.key`, 64 | certPath: `${directory}domains/${domain}.crt` 65 | }; 66 | }; 67 | 68 | let readKeyPair = async(function* (domain) { 69 | let { keyPath, certPath } = keyPairPaths(domain); 70 | let [key, certificate] = yield Promise.all([fs.readFileAsync(keyPath), fs.readFileAsync(certPath)]); 71 | 72 | return yield Promise.resolve({ key, certificate }); 73 | }); 74 | 75 | let isCertificateValid = async(function* (certPem) { 76 | const certificate = pki.certificateFromPem(certPem); 77 | return certificate.validity.notAfter > Date.now(); 78 | }); 79 | 80 | let getServerCertificate = async(function* (domain) { 81 | if(domainCertificates[domain]) { 82 | return yield Promise.resolve(domainCertificates[domain]); 83 | } 84 | 85 | let { keyPath, certPath } = keyPairPaths(domain); 86 | 87 | let result = null; 88 | let found = yield helpers.exists(keyPath, certPath); 89 | if(found) { 90 | result = yield readKeyPair(domain); 91 | } 92 | 93 | if(!result || !isCertificateValid(result.certificate)) { 94 | yield mkdirp(directory + 'domains'); 95 | if(!fs.existsSync(caCertPath)) { 96 | yield createCertificateAuthority(); 97 | } else { 98 | ca = yield fs.readFileAsync(caCertPath); 99 | } 100 | 101 | const csr = pki.createCertificationRequest(); 102 | csr.publicKey = keys.publicKey; 103 | const attrs = [{ 104 | name: 'commonName', 105 | value: domain 106 | }, { 107 | name: 'organizationName', 108 | value: 'do_not_trust_phoenixmatrix' 109 | }, { 110 | name: 'countryName', 111 | value: 'US' 112 | }]; 113 | 114 | csr.setSubject(attrs); 115 | csr.sign(keys.privateKey, md.sha256.create()); 116 | 117 | const caKeyPem = yield fs.readFileAsync(caKeyPath); 118 | const caKey = pki.privateKeyFromPem(caKeyPem); 119 | const caCertPem = yield fs.readFileAsync(caCertPath); 120 | const caCert = pki.certificateFromPem(caCertPem); 121 | 122 | var certificate = pki.createCertificate(); 123 | certificate.serialNumber = getSerial(); 124 | certificate.validity.notBefore = new Date(); 125 | 126 | const expiration = moment(certificate.validity.notBefore); 127 | expiration.add(config.certificateExpiration, 'days'); 128 | certificate.validity.notAfter = expiration.toDate(); 129 | 130 | certificate.setSubject(csr.subject.attributes); 131 | certificate.setIssuer(caCert.subject.attributes); 132 | certificate.publicKey = csr.publicKey; 133 | certificate.sign(caKey, md.sha256.create()); 134 | 135 | const serverCertificate = pki.certificateToPem(certificate); 136 | const serverKey = pki.privateKeyToPem(keys.privateKey); 137 | 138 | yield Promise.all([ 139 | fs.writeFileAsync(certPath, serverCertificate), 140 | fs.writeFileAsync(keyPath, serverKey) 141 | ]); 142 | 143 | result = yield Promise.resolve({key: serverKey, certificate: serverCertificate}); 144 | } 145 | 146 | domainCertificates[domain] = result; 147 | return yield Promise.resolve(result); 148 | }); 149 | 150 | export default { 151 | createCertificateAuthority, 152 | getServerCertificate, 153 | getCA 154 | } 155 | -------------------------------------------------------------------------------- /src/lib/config.js: -------------------------------------------------------------------------------- 1 | export default require('../../phoenixmatrix.json'); 2 | -------------------------------------------------------------------------------- /src/lib/helpers.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import Promise from 'bluebird'; 3 | 4 | let stat = Promise.promisify(fs.stat); 5 | 6 | let exists = function (...paths) { 7 | return new Promise(function(resolve) { 8 | let fileStats = paths.map((path) => stat(path)); 9 | Promise.all(fileStats).then(() => resolve(true)).catch(() => resolve(false)); 10 | }); 11 | }; 12 | 13 | export default { exists }; -------------------------------------------------------------------------------- /src/lib/proxyFactory.js: -------------------------------------------------------------------------------- 1 | import net from 'net'; 2 | import http from 'http'; 3 | import https from 'https'; 4 | import connect from 'connect'; 5 | import tls from 'tls'; 6 | import crypto from 'crypto'; 7 | import uuid from 'node-uuid'; 8 | import zlib from 'zlib'; 9 | import url from 'url'; 10 | 11 | import Promise from 'bluebird'; 12 | import cert from './certificate'; 13 | import config from './config'; 14 | 15 | var requestProcessors = []; 16 | 17 | var handleRequest = function(server, req, res, processors) { 18 | if(processors.length) { 19 | processors[0].call(server, req, res, handleRequest.bind(null, server, req, res, processors.slice(1))); 20 | } 21 | }; 22 | 23 | requestProcessors.push(function (req, res, next) { 24 | req.id = uuid.v4(); 25 | req.body = ''; 26 | 27 | req.on('error', function(err) { 28 | console.log(err); 29 | }); 30 | 31 | if(req.method === 'POST') { 32 | req.on('data', function (chunk) { 33 | req.body += chunk; 34 | }); 35 | } 36 | next(); 37 | }); 38 | 39 | requestProcessors.push(function(req, res, next) { 40 | var parsedUrl = url.parse(req.url); 41 | var emitRequest = () => { 42 | this.emit('proxyRequest', { 43 | id: req.id, 44 | url: req.url, 45 | path: parsedUrl.path, 46 | port: req.port, 47 | method: req.method, 48 | headers: req.headers, 49 | isSSL: req.isSSL, 50 | host: req.headers ? req.headers.host : '', 51 | body: req.body, 52 | timestamp: new Date().getTime(), 53 | state: 'request' 54 | }); 55 | }; 56 | 57 | if(req.method === 'POST') { 58 | req.on('end', function() { 59 | emitRequest(); 60 | }); 61 | } else { 62 | emitRequest(); 63 | } 64 | 65 | next(); 66 | }); 67 | 68 | let emitResponse = function(server, req, res, body) { 69 | server.emit('proxyResponse', { 70 | id: req.id, 71 | url: req.url, 72 | port: req.port, 73 | method: req.method, 74 | responseHeaders: res.headers, 75 | error: req.error, 76 | state: 'response', 77 | statusCode: req.error && res.statusCode === 200 ? null : res.statusCode, 78 | responseBody: body && body.length > 0 ? body : null 79 | }); 80 | }; 81 | 82 | requestProcessors.push(function(req, res, next) { 83 | var server = this; 84 | req.on('response', function(response) { 85 | var responseDecoder = response; 86 | 87 | if(response.headers['content-encoding'] === 'gzip') { 88 | var gunzip = zlib.createGunzip(); 89 | responseDecoder = gunzip; 90 | response.pipe(gunzip); 91 | } 92 | 93 | var responseBody = ''; 94 | responseDecoder.on('data', function(chunk) { 95 | responseBody += chunk; 96 | }); 97 | 98 | responseDecoder.on('end', function() { 99 | emitResponse(server, req, response, responseBody); 100 | }); 101 | }); 102 | 103 | next(); 104 | }); 105 | 106 | requestProcessors.push(function (req, res, next) { 107 | let server = this; 108 | var parts = req.headers.host.split(':', 2); 109 | var options = {}; 110 | 111 | ['method', 'headers', 'host', 'hostname'].forEach(function (property) { 112 | if (req[property]) { 113 | options[property] = req[property]; 114 | } 115 | }); 116 | 117 | var parsedUrl = url.parse(req.url); 118 | var isSSL = req.isSSL; 119 | 120 | options.rejectUnauthorized = false; 121 | options.agent = false; 122 | options.host = parts[0]; 123 | options.path = parsedUrl.path; 124 | options.port = parts[1] || (isSSL ? 443 : 80); 125 | if(isSSL) { 126 | options.secureProtocol = 'TLSv1_method'; 127 | } 128 | 129 | var forwardRequest = isSSL ? https.request(options) : http.request(options); 130 | 131 | req.on('aborted', function () { 132 | console.log('aborted!'); 133 | forwardRequest.abort(); 134 | }); 135 | 136 | forwardRequest.on('error', function (err) { 137 | console.log(err); 138 | forwardRequest.abort(); 139 | if(err.code === "ENOTFOUND") { 140 | res.statusCode = 400; 141 | } 142 | 143 | req.error = true; 144 | 145 | res.end(); 146 | emitResponse(server, req, res); 147 | }); 148 | 149 | forwardRequest.on('close', function() { 150 | forwardRequest.connection.end(); 151 | }); 152 | 153 | forwardRequest.on('response', function (response) { 154 | if (!response.headers.connection) { 155 | response.headers.connection = req.headers.connection || 'keep-alive'; 156 | } 157 | 158 | Object.keys(response.headers).forEach(function (key) { 159 | res.setHeader(key, response.headers[key]); 160 | }); 161 | 162 | response.on('end', function() { 163 | forwardRequest.connection.end(); 164 | res.end(); 165 | }); 166 | 167 | res.writeHead(response.statusCode); 168 | req.emit('response', response); 169 | response.pipe(res, {end:true}); 170 | }); 171 | 172 | req.pipe(forwardRequest); 173 | next(); 174 | }); 175 | 176 | var getSecureContext = function(keys) { 177 | return tls.createSecureContext({ 178 | key: keys.key, 179 | cert: keys.certificate, 180 | ca: cert.getCA() 181 | }); 182 | }; 183 | 184 | var certCache = {}; 185 | 186 | var sniCallback = (hostname, cb) => cb(null, getSecureContext(certCache[hostname])); 187 | 188 | var createHttpsProxy = function(options) { 189 | return new Promise(function(resolve) { 190 | cert.getServerCertificate('localhost').then(function(keys) { 191 | certCache['localhost'] = keys; 192 | var httpsProxyServer = https.createServer({SNICallback: sniCallback, key: keys.key, cert: keys.certificate, ca: cert.getCA()}, function(req, res) { 193 | req.isSSL = true; 194 | handleRequest(httpsProxyServer, req, res, requestProcessors); 195 | }); 196 | 197 | httpsProxyServer.listen(options.port, function() { 198 | console.log('Proxy listening on port %d', httpsProxyServer.address().port); 199 | }); 200 | 201 | httpsProxyServer.on('error', function(err) { 202 | console.log(err); 203 | }); 204 | 205 | resolve(httpsProxyServer); 206 | }); 207 | }); 208 | }; 209 | 210 | var createHttpProxy = function(options) { 211 | return new Promise(function(resolve) { 212 | var proxyServer = http.createServer(function (req, res) { 213 | handleRequest(proxyServer, req, res, requestProcessors); 214 | }); 215 | 216 | proxyServer.on('connect', function (req, socket, head) { 217 | var parts = req.url.split(':', 2); 218 | var targetHost = parts[0]; 219 | 220 | var port = this.httpsProxyPort ? this.httpsProxyPort : parts[1]; 221 | var host = this.httpsProxyPort ? 'localhost' : parts[0]; 222 | 223 | var handleConnect = function () { 224 | var conn = net.connect(port, host, function () { 225 | socket.setNoDelay(); 226 | socket.write("HTTP/1.1 200 OK\r\n\r\n"); 227 | socket.pipe(conn); 228 | conn.pipe(socket); 229 | }); 230 | 231 | socket.on('error', function (err) { 232 | console.log(err); 233 | conn.end(); 234 | }); 235 | 236 | conn.on('error', function (err) { 237 | console.log(err); 238 | socket.end(); 239 | }); 240 | 241 | if(config.includeConnect) { 242 | proxyServer.emit('proxyConnect', { 243 | id: uuid.v4(), 244 | url: parts[0], 245 | host: parts[0], 246 | method: 'CONNECT', 247 | port: parts[1], 248 | timestamp: new Date().getTime(), 249 | state: 'connect' 250 | }); 251 | } 252 | }; 253 | 254 | if (!certCache[targetHost]) { 255 | cert.getServerCertificate(targetHost).then(function (keys) { 256 | certCache[targetHost] = keys; 257 | handleConnect(); 258 | }); 259 | } else { 260 | handleConnect(); 261 | } 262 | }); 263 | 264 | proxyServer.on('error', function (err) { 265 | console.log(err); 266 | }); 267 | 268 | proxyServer.enableHttpsProxy = function (httpsProxy) { 269 | this.httpsProxyPort = httpsProxy.address().port; 270 | }; 271 | 272 | proxyServer.disableHttpsProxy = function () { 273 | delete this.httpsProxyPort; 274 | }; 275 | 276 | proxyServer.listen(options.port, function () { 277 | console.log('Proxy listening on port %d', proxyServer.address().port); 278 | }); 279 | 280 | resolve(proxyServer); 281 | }); 282 | }; 283 | 284 | export default {createHttpProxy, createHttpsProxy}; 285 | -------------------------------------------------------------------------------- /src/lib/pure.js: -------------------------------------------------------------------------------- 1 | import shallowCompare from 'react-addons-shallow-compare'; 2 | 3 | export default (Component) => { 4 | Component.prototype.shouldComponentUpdate = function (nextProps, nextState) { 5 | return shallowCompare(this, nextProps, nextState); 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-console: 0 */ 4 | 5 | const globalShortcut = require('global-shortcut'); 6 | 7 | process.on('error', function (err) { 8 | console.log(err); 9 | }); 10 | 11 | const app = require('app'); 12 | const BrowserWindow = require('browser-window'); 13 | 14 | let mainWindow; 15 | 16 | app.on('window-all-closed', () => { 17 | if (process.platform !== 'darwin') { 18 | app.quit(); 19 | } 20 | }); 21 | 22 | app.on('ready', function () { 23 | globalShortcut.register('CommandOrControl+Shift+i', () => { 24 | const w = BrowserWindow.getFocusedWindow(); 25 | if (w) { 26 | w.toggleDevTools(); 27 | } 28 | }); 29 | 30 | mainWindow = new BrowserWindow({width: 1000, height: 800, icon: './icon.png'}); 31 | mainWindow.setMenu(null); 32 | 33 | mainWindow.loadURL('file://' + __dirname + '/../index.html'); 34 | mainWindow.on('closed', () => { 35 | mainWindow = null; 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/reducers/config.js: -------------------------------------------------------------------------------- 1 | import {LOAD_CONFIG, TOGGLE_CONNECT} from '../actions/config'; 2 | 3 | const initialState = { 4 | includeConnect: false 5 | }; 6 | 7 | export default function config(state = initialState, action) { 8 | switch (action.type) { 9 | case LOAD_CONFIG: 10 | return {...state, ...action.config, configLoaded: true}; 11 | case TOGGLE_CONNECT: 12 | return {...state, includeConnect: action.includeConnect}; 13 | default: 14 | return state; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | import config from './config'; 3 | import requests from './requests'; 4 | 5 | const rootReducer = combineReducers({ 6 | config, 7 | requests 8 | }); 9 | 10 | export default rootReducer; 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/reducers/requests.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import {SELECT_REQUEST, UPDATE_REQUEST, SET_FILTER, CLEAR, TOGGLE_PAUSE} from '../actions/requests'; 3 | 4 | const initialState = { 5 | requests: [], 6 | filteredRequests: [], 7 | requestsMap: {} 8 | }; 9 | 10 | const isMatch = (filter, request) => { 11 | return (request.url && request.url.indexOf(filter) > -1) || 12 | (request.host && request.host.indexOf(filter) > -1) || 13 | (request.statusCode && request.statusCode.toString().indexOf(filter) > -1) || 14 | (request.method && request.method.indexOf(filter) > -1); 15 | }; 16 | 17 | const updateRequest = (state, action) => { 18 | if (state.paused || !action.request.id) { 19 | return state; 20 | } 21 | 22 | const requestsMap = {...state.requestsMap}; 23 | const filteredRequests = [...state.filteredRequests]; 24 | const filter = state.filter; 25 | const request = action.request; 26 | const original = requestsMap[request.id]; 27 | const requests = [...state.requests]; 28 | 29 | if (original) { 30 | const updatedRequest = {...original, ...request}; 31 | requestsMap[request.id] = updatedRequest; 32 | const filteredIndex = filteredRequests.lastIndexOf(request.id); 33 | if (!filter || isMatch(filter, updatedRequest)) { 34 | if (filteredIndex < 0) { 35 | filteredRequests.push(request.id); 36 | } 37 | } else if (filteredIndex > 0) { 38 | filteredRequests.splice(filteredIndex, 1); 39 | } 40 | // the user may have paused, and this is a response for a request we never caught 41 | } else if (request.state !== 'response') { 42 | requestsMap[request.id] = request; 43 | requests.push(request.id); 44 | if (!filter || isMatch(filter, request)) { 45 | filteredRequests.push(request.id); 46 | } 47 | } 48 | 49 | return {...state, requestsMap, requests, filteredRequests}; 50 | }; 51 | 52 | export default function config(state = initialState, action) { 53 | switch (action.type) { 54 | case SELECT_REQUEST: 55 | return {...state, selectedRequest: state.requestsMap[action.request.id]}; 56 | case UPDATE_REQUEST: 57 | return updateRequest(state, action); 58 | case SET_FILTER: 59 | const filter = action.filter; 60 | const filteredRequests = filter.length 61 | ? state.requests.filter(id => isMatch(filter, state.requestsMap[id])) 62 | : [...state.requests]; 63 | return {...state, filter, filteredRequests}; 64 | case CLEAR: 65 | return {...state, ...initialState}; 66 | case TOGGLE_PAUSE: 67 | return {...state, paused: !state.paused}; 68 | default: 69 | return state; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /stylesheets/splitter.less: -------------------------------------------------------------------------------- 1 | .split-panes 2 | { 3 | left: 0px; 4 | right: 0px; 5 | top: 0px; 6 | bottom: 0px; 7 | position: absolute; 8 | } 9 | 10 | .split-panes > .split-handler 11 | { 12 | background: transparent; 13 | position: absolute; 14 | z-index: 999; 15 | } 16 | 17 | /* Horizontal */ 18 | 19 | .split-panes.horizontal > .split-handler 20 | { 21 | width: 4px; 22 | top: 0px; 23 | left: 50%; 24 | bottom: 0px; 25 | cursor: ew-resize; 26 | } 27 | 28 | .split-panes.horizontal > .split-pane1, 29 | .split-panes.horizontal > .split-pane2 30 | { 31 | position: absolute; 32 | height: 100%; 33 | } 34 | 35 | .split-panes.horizontal > .split-pane1 36 | { 37 | width: 50%; 38 | } 39 | 40 | .split-panes.horizontal > .split-pane2 41 | { 42 | left: 50%; 43 | right: 0px; 44 | border-left: 1px solid #aaa; 45 | } 46 | 47 | 48 | /* Vertical */ 49 | 50 | .split-panes.vertical > .split-handler 51 | { 52 | height: 4px; 53 | top: 50%; 54 | left: 0px; 55 | right: 0px; 56 | cursor: ns-resize; 57 | } 58 | 59 | .split-panes.vertical > .split-pane1, 60 | .split-panes.vertical > .split-pane2 61 | { 62 | position: absolute; 63 | width: 100%; 64 | } 65 | 66 | .split-panes.vertical > .split-pane1 67 | { 68 | height: 50%; 69 | } 70 | 71 | .split-panes.vertical > .split-pane2 72 | { 73 | top: 50%; 74 | bottom: 0px; 75 | border-top: 1px solid #aaa; 76 | } -------------------------------------------------------------------------------- /stylesheets/style.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../vendor/bootstrap/less/mixins.less"; 2 | @import (reference) "../vendor/bootstrap/less/variables.less"; 3 | 4 | @splitPosition: 460px; 5 | 6 | body, html { 7 | width: 100%; 8 | height: 100%; 9 | margin: 0px; 10 | overflow: hidden; 11 | } 12 | 13 | .view-wrapper, .main-section { 14 | width: 100%; 15 | height: 100%; 16 | } 17 | 18 | .header-section { 19 | padding-right: 15px; 20 | } 21 | 22 | .left-section { 23 | width: 100%; 24 | position: absolute; 25 | overflow: hidden; 26 | top: 50px; 27 | bottom: 20px; 28 | 29 | .button-toolbar { 30 | width: 80px; 31 | background-color: #f8f8f8; 32 | border-right: 1px solid #a9a9a9; 33 | height: 100%; 34 | float: left; 35 | display: block; 36 | padding-left: 0px; 37 | li { 38 | cursor: pointer; 39 | display: block; 40 | height: 80px; 41 | padding: 25px; 42 | font-size: 30px; 43 | border-bottom: 1px solid darkgray; 44 | -webkit-font-smoothing: antialiased; 45 | text-rendering: auto; 46 | 47 | &.selected { 48 | background-color: @brand-primary; 49 | color: #fff; 50 | } 51 | } 52 | } 53 | 54 | .requests { 55 | height: 100%; 56 | } 57 | 58 | .request-list-scroll { 59 | position: relative; 60 | overflow: auto; 61 | height: 100%; 62 | overflow-x: hidden; 63 | } 64 | 65 | ul.request-list { 66 | list-style-type: none; 67 | display: block; 68 | position: absolute; 69 | top: 0px; 70 | width: 100%; 71 | margin-bottom: 0px; 72 | padding-left: 0px; 73 | 74 | li.request-item { 75 | cursor: pointer; 76 | padding: 5px; 77 | border-bottom: 1px dotted black; 78 | white-space: nowrap; 79 | &:hover, &.selected-request, &.selected-request:hover { 80 | background-color: @brand-primary; 81 | color: #fff; 82 | 83 | &.error-request { 84 | color: #ff0000; 85 | } 86 | } 87 | 88 | &.error-request { 89 | color: #ff0000; 90 | } 91 | 92 | &.cached-request { 93 | opacity: 0.5; 94 | } 95 | } 96 | 97 | .request-properties > span { 98 | display: inline-block; 99 | margin-right: 3px; 100 | font-weight: bold; 101 | } 102 | 103 | .request-status-code { 104 | width: 24px; 105 | } 106 | 107 | .request-method { 108 | min-width: 67px; 109 | } 110 | 111 | .request-path { 112 | overflow: hidden; 113 | white-space:nowrap; 114 | text-overflow: ellipsis; 115 | } 116 | } 117 | 118 | .pending-request { 119 | opacity: 0.5 120 | } 121 | } 122 | 123 | .content-section { 124 | width: 100%; 125 | overflow-y: auto; 126 | overflow-x: hidden; 127 | padding: 10px; 128 | position: absolute; 129 | top: 50px; 130 | bottom: 20px; 131 | right: 0px; 132 | } 133 | 134 | .footer-section { 135 | position: absolute; 136 | bottom: 0px; 137 | width: 100%; 138 | height: 20px; 139 | background-color: #3b3b3b; 140 | border-top: 3px solid #428bca; 141 | } 142 | 143 | .headers-list { 144 | overflow: hidden; 145 | 146 | .panel-heading { 147 | border-bottom: 0px; 148 | } 149 | } 150 | 151 | .headers-list, .request-body, .response-body { 152 | .panel-heading { 153 | cursor: pointer; 154 | -moz-user-select: none; 155 | -webkit-user-select: none; 156 | -ms-user-select: none; 157 | } 158 | 159 | .panel-body { 160 | word-break: break-all; 161 | } 162 | } 163 | 164 | .response-body { 165 | textarea { 166 | width: 100%; 167 | height: 300px; 168 | } 169 | } 170 | 171 | /* -- overrides for initial positioning of the split handle */ 172 | .main-section .split-panes.horizontal { 173 | & > .split-handler { 174 | left: @splitPosition; 175 | } 176 | 177 | & > .split-pane1 { 178 | width: @splitPosition; 179 | } 180 | 181 | & > .split-pane2 { 182 | border-left: 3px solid #428bca; 183 | left: @splitPosition; 184 | } 185 | } 186 | 187 | /* -- request header -- */ 188 | .request-header { 189 | max-height: 200px; 190 | overflow-y: auto; 191 | 192 | span { 193 | word-wrap:break-word; 194 | } 195 | 196 | .request-properties { 197 | font-weight: bold; 198 | } 199 | } 200 | 201 | 202 | -------------------------------------------------------------------------------- /vendor/bootstrap/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.2.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default:disabled,.btn-default[disabled]{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-o-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#2d6ca2));background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#2b669a}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-primary:disabled,.btn-primary[disabled]{background-color:#2d6ca2;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-success:disabled,.btn-success[disabled]{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-info:disabled,.btn-info[disabled]{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-warning:disabled,.btn-warning[disabled]{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-danger:disabled,.btn-danger[disabled]{background-color:#c12e2a;background-image:none}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f3f3f3));background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:-o-linear-gradient(top,#222 0,#282828 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#222),to(#282828));background-image:linear-gradient(to bottom,#222 0,#282828 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-o-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3071a9));background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-o-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3278b3));background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);background-repeat:repeat-x;border-color:#3278b3}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /vendor/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenixmatrix/phoenixmatrix-proxy/0edb2448571e4627a1c51bf571998e2db86963dc/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenixmatrix/phoenixmatrix-proxy/0edb2448571e4627a1c51bf571998e2db86963dc/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /vendor/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenixmatrix/phoenixmatrix-proxy/0edb2448571e4627a1c51bf571998e2db86963dc/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /vendor/bootstrap/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.2.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.2.0",d.prototype.close=function(b){function c(){f.detach().trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one("bsTransitionEnd",c).emulateTransitionEnd(150):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.2.0",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),d[e](null==f[b]?this.options[b]:f[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b).on("keydown.bs.carousel",a.proxy(this.keydown,this)),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.2.0",c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},c.prototype.keydown=function(a){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.to=function(b){var c=this,d=this.getItemIndex(this.$active=this.$element.find(".item.active"));return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=e[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:g});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,f&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(e)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:g});return a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one("bsTransitionEnd",function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger(m)),f&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(b=!b),e||d.data("bs.collapse",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};c.VERSION="3.2.0",c.DEFAULTS={toggle:!0},c.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},c.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var c=a.Event("show.bs.collapse");if(this.$element.trigger(c),!c.isDefaultPrevented()){var d=this.$parent&&this.$parent.find("> .panel > .in");if(d&&d.length){var e=d.data("bs.collapse");if(e&&e.transitioning)return;b.call(d,"hide"),e||d.data("bs.collapse",null)}var f=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[f](0),this.transitioning=1;var g=function(){this.$element.removeClass("collapsing").addClass("collapse in")[f](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return g.call(this);var h=a.camelCase(["scroll",f].join("-"));this.$element.one("bsTransitionEnd",a.proxy(g,this)).emulateTransitionEnd(350)[f](this.$element[0][h])}}},c.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},c.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var d=a.fn.collapse;a.fn.collapse=b,a.fn.collapse.Constructor=c,a.fn.collapse.noConflict=function(){return a.fn.collapse=d,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(c){var d,e=a(this),f=e.attr("data-target")||c.preventDefault()||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),g=a(f),h=g.data("bs.collapse"),i=h?"toggle":e.data(),j=e.attr("data-parent"),k=j&&a(j);h&&h.transitioning||(k&&k.find('[data-toggle="collapse"][data-parent="'+j+'"]').not(e).addClass("collapsed"),e[g.hasClass("in")?"addClass":"removeClass"]("collapsed")),b.call(g,i)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.2.0",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('