├── logs ├── change.log └── access.log ├── scripts └── es5-server.js ├── Procfile ├── .gitignore ├── stubs ├── !truthy! │ ├── true.json │ ├── ambiguous.json │ └── false.json ├── !users! │ ├── admins.json │ └── users.json ├── !id! │ ├── id.json │ └── newStub.json └── !index! │ ├── index.html │ └── index.xml ├── .babelrc ├── .travis.yml ├── bin └── mocknode.js ├── dist ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ ├── glyphicons-halflings-regular.woff2 │ └── glyphicons-halflings-regular.svg ├── index.html ├── css │ ├── styles.css │ └── bootstrap-theme.min.css └── script │ └── fetch.js ├── src ├── Pages.jsx ├── webpack.config.js ├── index.jsx ├── header.jsx ├── stubs.jsx ├── routing.jsx ├── stubs-dynamic.jsx ├── stubForm.jsx ├── Store.js ├── routingManager.jsx ├── stubForm-dynamic.jsx └── server.es6 ├── package.json ├── config.json ├── README.md └── server.js /logs/change.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/es5-server.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tmp 3 | -------------------------------------------------------------------------------- /stubs/!truthy!/true.json: -------------------------------------------------------------------------------- 1 | { 2 | "return": true 3 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets" : ["es2015", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" -------------------------------------------------------------------------------- /stubs/!truthy!/ambiguous.json: -------------------------------------------------------------------------------- 1 | { 2 | "return": null 3 | } -------------------------------------------------------------------------------- /stubs/!truthy!/false.json: -------------------------------------------------------------------------------- 1 | { 2 | "return": false 3 | } -------------------------------------------------------------------------------- /bin/mocknode.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("../server.js"); 4 | -------------------------------------------------------------------------------- /dist/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianunay/mock-node/HEAD/dist/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /dist/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianunay/mock-node/HEAD/dist/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /dist/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianunay/mock-node/HEAD/dist/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /dist/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianunay/mock-node/HEAD/dist/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /logs/access.log: -------------------------------------------------------------------------------- 1 | {"route":"/id/","query strings":"{}","request body":"{}","ip":"::ffff:127.0.0.1","level":"info","message":"","timestamp":"2016-05-07T17:47:28.028Z"} 2 | -------------------------------------------------------------------------------- /stubs/!users!/admins.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "user": "Anunay" 4 | }, 5 | { 6 | "user": "Inuganti" 7 | }, 8 | { 9 | "user": "anu" 10 | }, 11 | { 12 | "user": "Inu" 13 | } 14 | ] -------------------------------------------------------------------------------- /stubs/!users!/users.json: -------------------------------------------------------------------------------- 1 | {"users":[ 2 | { 3 | "user": "Anunay" 4 | }, 5 | { 6 | "user": "Inuganti" 7 | }, 8 | { 9 | "user": "anu" 10 | }, 11 | { 12 | "user": "Inu" 13 | } 14 | ]} -------------------------------------------------------------------------------- /stubs/!id!/id.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": "56asjdhkasjdhkajshdkjashdd" 4 | }, 5 | { 6 | "_id": "123127af5583cf16c3c590619" 7 | }, 8 | { 9 | "_id": "8ad213af5ffcb565e7b167f71" 10 | }, 11 | { 12 | "_id": "9asd9ad7423ed9c6cc6e483c" 13 | }, 14 | { 15 | "_id": "56c07af56e4f7d364fc35f8d" 16 | } 17 | ] -------------------------------------------------------------------------------- /stubs/!index!/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example file 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | This is an example html file 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /stubs/!id!/newStub.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourcePort": 3000, 3 | "routes": [ 4 | { 5 | "route": "/users", 6 | "proxy": "http://localhost:8090" 7 | }, 8 | { 9 | "route": "/id", 10 | "proxy": "http://localhost:8090" 11 | }, 12 | { 13 | "route": "/user", 14 | "proxy": "http://localhost:8010" 15 | }, 16 | { 17 | "route": "/admins", 18 | "stub": "admins" 19 | }, 20 | { 21 | "route": "/new/", 22 | "stub": "id" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /src/Pages.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Store from './Store.js'; 3 | 4 | const CHANGE_EVENT = 'PAGE_CHANGE_EVENT'; 5 | 6 | class Pages extends React.Component { 7 | constructor(props, context) { 8 | super(props, context); 9 | this.changePage = this.changePage.bind(this); 10 | this.state = { 11 | activePage: 1 12 | } 13 | }; 14 | componentDidMount(){ 15 | Store.on(CHANGE_EVENT, this.changePage); 16 | } 17 | changePage(activePage){ 18 | this.setState({activePage}); 19 | } 20 | render(){ 21 | return this.props.children[this.state.activePage - 1]; 22 | } 23 | } 24 | 25 | module.exports = Pages; -------------------------------------------------------------------------------- /src/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | var BUILD_DIR = path.resolve(__dirname, '../dist/script'); 5 | var APP_DIR = path.resolve(__dirname); 6 | 7 | var config = { 8 | entry: APP_DIR + '/index.jsx', 9 | output: { 10 | path: BUILD_DIR, 11 | filename: 'bundle.js' 12 | }, 13 | module : { 14 | loaders : [ 15 | { 16 | test : /\.jsx?/, 17 | include : APP_DIR, 18 | loader : 'babel' 19 | } 20 | ] 21 | }, 22 | resolve: { 23 | root: path.resolve(__dirname), 24 | alias: { 25 | store: './Store' 26 | }, 27 | extensions: ['', '.js', '.jsx'] 28 | } 29 | }; 30 | 31 | module.exports = config; -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | import Header from './header.jsx'; 4 | import Routing from './routing.jsx'; 5 | import Stubs from './stubs.jsx'; 6 | import DynamicStubs from './stubs-dynamic.jsx'; 7 | import Pages from './Pages.jsx'; 8 | 9 | 10 | class App extends React.Component { 11 | constructor(props, context) { 12 | super(props, context); 13 | }; 14 | render(){ 15 | return ( 16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 | ) 27 | } 28 | } 29 | 30 | render(, document.getElementById('app')); -------------------------------------------------------------------------------- /stubs/!index!/index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gambardella, Matthew 5 | XML Developer's Guide 6 | Computer 7 | 44.95 8 | 2000-10-01 9 | An in-depth look at creating applications 10 | with XML. 11 | 12 | 13 | Ralls, Kim 14 | Midnight Rain 15 | Fantasy 16 | 5.95 17 | 2000-12-16 18 | A former architect battles corporate zombies, 19 | an evil sorceress, and her own childhood to become queen 20 | of the world. 21 | 22 | -------------------------------------------------------------------------------- /src/header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Navbar, Nav, NavDropdown, MenuItem} from 'react-bootstrap'; 3 | 4 | class Header extends React.Component { 5 | constructor(props, context) { 6 | super(props, context); 7 | }; 8 | render(){ 9 | return ( 10 | 11 | 12 | 13 | Mock Node 14 | 15 | 16 | 22 | 23 | ); 24 | } 25 | } 26 | 27 | module.exports = Header; 28 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mock Node configuration 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 27 |
28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /dist/css/styles.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Montserrat:400,700); 2 | 3 | body { 4 | background-color: #f2f2f2; 5 | font-family: 'Montserrat', sans-serif; 6 | font-weight: 400; 7 | font-size: 14px; 8 | color: #555; 9 | 10 | -webkit-font-smoothing: antialiased; 11 | -webkit-overflow-scrolling: touch; 12 | } 13 | /* Titles */ 14 | h1, h2, h3, h4, h5, h6 { 15 | font-family: 'Montserrat', sans-serif; 16 | font-weight: 700; 17 | color: #333; 18 | } 19 | 20 | .container { 21 | width: 100%; 22 | } 23 | .navbar-inverse { 24 | background-color: #2f2f2f; 25 | border-radius: 0px; 26 | padding: 20px; 27 | } 28 | .navbar-inverse .navbar-nav>.active>a { 29 | background: none; 30 | box-shadow: none; 31 | } 32 | .route-panel .route { 33 | color: #333; 34 | display: inline-block; 35 | padding-left: 35px; 36 | } 37 | .route-panel a { 38 | display: block; 39 | text-decoration: none; 40 | } 41 | .panel-body a.link { 42 | display: inline-block; 43 | position: absolute; 44 | top: 0; 45 | right: 15px; 46 | } 47 | .route-panel a:hover .route { 48 | text-decoration: underline; 49 | } 50 | .route-panel.danger .label { 51 | padding: .2em 1.8em .3em; 52 | } 53 | .route-panel.warning .label { 54 | padding: .2em 1.3em .3em; 55 | } 56 | .route-panel.info .label { 57 | padding: .2em 1.6em .3em; 58 | } 59 | .contentInput { 60 | position: relative; 61 | } 62 | .contentInput a.link { 63 | position: absolute; 64 | right: 0; 65 | top: 0; 66 | } 67 | .backButton { 68 | display: inline-block; 69 | margin-top: 20px; 70 | font-size: 20px; 71 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mocknode", 3 | "version": "1.2.3", 4 | "description": "A configurable mock server with an intuitive configuration management interface and a http api", 5 | "author": "Anunay Inuganti", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/ianunay/mock-node.git" 10 | }, 11 | "main": "server.js", 12 | "engines": { 13 | "node": ">=0.12.0" 14 | }, 15 | "scripts": { 16 | "test": "echo \"Warn: tests to be written\"", 17 | "build-interface": "webpack -p --config src/webpack.config.js", 18 | "build-server": "babel src/server.es6 > server.js" 19 | }, 20 | "bin": { 21 | "mocknode": "./bin/mocknode.js" 22 | }, 23 | "keywords": [ 24 | "mock", 25 | "dynamic", 26 | "dynamic mocks", 27 | "webserver", 28 | "reverse proxy", 29 | "proxy", 30 | "nodejs", 31 | "server" 32 | ], 33 | "devDependencies": { 34 | "babel-cli": "^6.7.5", 35 | "babel-core": "^6.5.2", 36 | "babel-loader": "^6.2.4", 37 | "babel-preset-es2015": "^6.6.0", 38 | "babel-preset-react": "^6.5.0", 39 | "react": "^0.14.7", 40 | "react-bootstrap": "^0.28.3", 41 | "react-dom": "^0.14.7", 42 | "valid-url": "^1.0.9", 43 | "webpack": "^1.12.14" 44 | }, 45 | "dependencies": { 46 | "async": "^1.5.2", 47 | "body-parser": "^1.15.0", 48 | "cookie-parser": "^1.4.2", 49 | "express": "^4.13.4", 50 | "express-http-proxy": "^0.6.0", 51 | "fs-extra": "^0.30.0", 52 | "minimist": "^1.2.0", 53 | "object-assign": "^4.0.1", 54 | "rimraf": "^2.5.2", 55 | "sandcastle": "^1.3.3", 56 | "tar-fs": "^1.12.0", 57 | "winston": "^2.2.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/stubs.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Store from 'store'; 3 | import {PageHeader, Tabs, Tab} from 'react-bootstrap'; 4 | import StubForm from './stubForm.jsx'; 5 | 6 | const config_get_event = 'CONFIG_FETCH_COMPLETE_EVENT', 7 | activate_tab_event = 'STUBS_ACTIVATE_TAB_EVENT'; 8 | 9 | class Stubs extends React.Component { 10 | constructor(props, context) { 11 | super(props, context); 12 | this.updateState = this.updateState.bind(this); 13 | this.activateTab = this.activateTab.bind(this); 14 | this.reRender = this.reRender.bind(this); 15 | 16 | 17 | this.state = { 18 | stubs: [], 19 | activeTab: 1 20 | }; 21 | }; 22 | componentWillMount(){ 23 | Store.on(config_get_event, this.updateState); 24 | Store.on(activate_tab_event, this.reRender); 25 | } 26 | componentDidMount(){ 27 | this.updateState(); 28 | } 29 | componentWillUnmount(){ 30 | Store.removeListener(config_get_event, this.updateState); 31 | Store.removeListener(activate_tab_event, this.reRender); 32 | } 33 | updateState(){ 34 | let stubs; 35 | for (var i = 0; i < Store.config.routes.length; i++) { 36 | if (Store.config.routes[i].route == Store.routeOfInterest) { 37 | stubs = Store.config.routes[i].stubs; 38 | break; 39 | } 40 | }; 41 | 42 | this.setState({stubs}); 43 | } 44 | reRender(){ 45 | let that = this; 46 | setTimeout(() => {that.activateTab(1)}, 100); 47 | } 48 | activateTab(key){ 49 | this.setState({activeTab: key}); 50 | } 51 | render(){ 52 | var tabs = []; 53 | tabs.push( 54 | 55 | ); 56 | this.state.stubs.map((stub, i) => { 57 | tabs.push( 58 | 59 | ) 60 | }); 61 | return ( 62 |
63 | Back to Routes 64 | Manage Stubs for {Store.routeOfInterest} 65 | 66 | {tabs} 67 | 68 |
69 | ) 70 | } 71 | } 72 | 73 | module.exports = Stubs; 74 | -------------------------------------------------------------------------------- /src/routing.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Store from 'store'; 3 | import {PageHeader, Accordion, Grid, Row, Col, Button, Label, Panel} from 'react-bootstrap'; 4 | import RoutingManager from './routingManager.jsx'; 5 | 6 | const config_get_event = 'CONFIG_FETCH_COMPLETE_EVENT'; 7 | 8 | class Routing extends React.Component { 9 | constructor(props, context) { 10 | super(props, context); 11 | this.updateState = this.updateState.bind(this); 12 | this.addRoute = this.addRoute.bind(this); 13 | this.deleteRoute = this.deleteRoute.bind(this); 14 | 15 | this.state = { 16 | routes: [], 17 | stublist: [], 18 | dynamiclist: [] 19 | }; 20 | }; 21 | componentWillMount(){ 22 | Store.getConfig(); 23 | Store.on(config_get_event, this.updateState); 24 | } 25 | componentWillUnmount() { 26 | Store.removeListener(config_get_event, this.updateState); 27 | } 28 | addRoute() { 29 | this.setState({count: this.state.routes.push({newRoute: true, stubs:[], dynamicStubs: []})}); 30 | } 31 | deleteRoute(i) { 32 | this.state.routes.splice(i, 1); 33 | this.setState({count: this.state.routes.length}); 34 | } 35 | updateState(){ 36 | this.setState({routes: Store.config.routes, count: Store.config.routes.length}); 37 | } 38 | render(){ 39 | let PanelArray = []; 40 | this.state.routes.map(function(route, i){ 41 | let panelStyle = route.newRoute ? "danger" : (route.handle == "proxy") ? "warning" : (route.handle == "stub") ? "info" : "success"; 42 | let header = ( 43 |

44 | 47 | {route.route || 'New Route'} 48 |

49 | ); 50 | PanelArray.push( 51 | 52 | 54 | 55 | ) 56 | }.bind(this)); 57 | PanelArray.push(); 58 | return ( 59 | 60 | 61 | 62 | Manage routes 63 | 64 | {PanelArray} 65 | 66 | 67 | 68 | 69 | ) 70 | } 71 | } 72 | 73 | module.exports = Routing; 74 | -------------------------------------------------------------------------------- /src/stubs-dynamic.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Store from 'store'; 3 | import {PageHeader, Tabs, Tab} from 'react-bootstrap'; 4 | import StubFormDynamic from './stubForm-dynamic.jsx'; 5 | 6 | const config_get_event = 'CONFIG_FETCH_COMPLETE_EVENT', 7 | activate_tab_event = 'DYNAMIC_STUBS_ACTIVATE_TAB_EVENT'; 8 | 9 | class Stubs extends React.Component { 10 | constructor(props, context) { 11 | super(props, context); 12 | this.updateState = this.updateState.bind(this); 13 | this.activateTab = this.activateTab.bind(this); 14 | this.reRender = this.reRender.bind(this); 15 | 16 | this.state = { 17 | dynamic: [], 18 | stublist: [], 19 | activeTab: 1 20 | }; 21 | }; 22 | componentWillMount(){ 23 | Store.on(config_get_event, this.updateState); 24 | Store.on(activate_tab_event, this.reRender); 25 | } 26 | componentDidMount(){ 27 | this.updateState(); 28 | } 29 | componentWillUnmount(){ 30 | Store.removeListener(config_get_event, this.updateState); 31 | Store.removeListener(activate_tab_event, this.reRender); 32 | } 33 | reRender(){ 34 | let that = this; 35 | setTimeout(() => {that.activateTab(1)}, 100); 36 | } 37 | updateState(){ 38 | let dynamic, 39 | stublist; 40 | for (var i = 0; i < Store.config.routes.length; i++) { 41 | if (Store.config.routes[i].route == Store.routeOfInterest) { 42 | dynamic = Store.config.routes[i].dynamicStubs; 43 | stublist = Store.config.routes[i].stubs; 44 | break; 45 | } 46 | }; 47 | this.setState({dynamic, stublist}); 48 | } 49 | activateTab(key){ 50 | this.setState({activeTab: key}); 51 | } 52 | render(){ 53 | var tabs = []; 54 | tabs.push( 55 | 56 | ); 57 | this.state.dynamic.map((stub, i) => { 58 | tabs.push( 59 | 60 | 63 | 64 | ) 65 | }); 66 | return ( 67 |
68 | Back to Routes 69 | Manage Dynamic Stubs for {Store.routeOfInterest} 70 | 71 | {tabs} 72 | 73 |
74 | ) 75 | } 76 | } 77 | 78 | module.exports = Stubs; 79 | -------------------------------------------------------------------------------- /src/stubForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Store from 'store'; 3 | import {Input, Button} from 'react-bootstrap'; 4 | 5 | const stub_get_event = 'STUB_GET_COMPLETE_EVENT'; 6 | 7 | class StubForm extends React.Component { 8 | constructor(props, context) { 9 | super(props, context); 10 | this.updateState = this.updateState.bind(this); 11 | this.handleChange = this.handleChange.bind(this); 12 | this.validate = this.validate.bind(this); 13 | this.postData = this.postData.bind(this); 14 | this.format = this.format.bind(this); 15 | this.deleteStub = this.deleteStub.bind(this); 16 | 17 | this.state = Object.assign({content: "", oldName: this.props.name}, this.props); 18 | }; 19 | componentWillMount(){ 20 | Store.on(stub_get_event, this.updateState); 21 | if (this.props.name) 22 | Store.getStub(this.props.name); 23 | } 24 | componentWillUnmount() { 25 | Store.removeListener(stub_get_event, this.updateState); 26 | } 27 | updateState(){ 28 | this.setState({content: Store.stubs[this.props.name]}); 29 | } 30 | handleChange(elem, event){ 31 | let partialState = {}; 32 | partialState[elem] = event ? event.target.value : window.event.target.value; 33 | this.setState(partialState); 34 | this.validate(Object.assign(this.state, partialState)); 35 | } 36 | validate(state){ 37 | this.setState({formValid: state.name && state.content}) 38 | } 39 | postData(){ 40 | Store.postStubData(this.state); 41 | } 42 | format(){ 43 | let content; 44 | try { 45 | content = JSON.stringify(JSON.parse(this.state.content), null, 2); 46 | } catch (e) {} 47 | if (content) 48 | this.setState({content: content}) 49 | } 50 | deleteStub(){ 51 | if (confirm("Are you sure you want to delete the stub : "+ this.props.name + " ?")) 52 | Store.deleteStub(this.props.name); 53 | } 54 | render(){ 55 | let actions = this.state.new 56 | ?
57 | : (
58 |
); 59 | return ( 60 |
61 | 63 | 65 |
66 | Content Format} value={this.state.content} 67 | onChange={this.handleChange.bind(this, "content")} onBlur={this.handleChange.bind(this, "content")}/> 68 |
69 | {actions} 70 |
71 | ) 72 | } 73 | } 74 | 75 | module.exports = StubForm; 76 | -------------------------------------------------------------------------------- /src/Store.js: -------------------------------------------------------------------------------- 1 | let EventEmitter = require('events').EventEmitter, 2 | assign = require('object-assign'); 3 | 4 | let Store = assign({}, EventEmitter.prototype, { 5 | config: {}, 6 | stubs: {}, 7 | routeOfInterest: "", 8 | updatePage: (page) => { 9 | Store.page = page; 10 | Store.emit('PAGE_CHANGE_EVENT', page); 11 | }, 12 | getConfig: () => { 13 | fetch('/mocknode/api/config').then((response) => { 14 | return response.json() 15 | }).then((json) => { 16 | Store.config = json; 17 | Store.emit('CONFIG_FETCH_COMPLETE_EVENT'); 18 | }) 19 | }, 20 | changeConfig: (config) => { 21 | let {old_route, route, handle, proxy, stub, stubs, dynamicStub, dynamicStubs} = config; 22 | 23 | fetch('/mocknode/api/modifyroute', { 24 | method: 'post', 25 | headers: { 26 | 'Accept': 'application/json', 27 | 'Content-Type': 'application/json' 28 | }, 29 | body: JSON.stringify({ 30 | old_route: old_route, 31 | route: route, 32 | handle: handle, 33 | proxy: proxy, 34 | stub: stub, 35 | dynamicStub: dynamicStub 36 | }) 37 | }).then((json) => { 38 | Store.getConfig(); 39 | }) 40 | 41 | }, 42 | getStub: (stub) => { 43 | fetch('/mocknode/api/getstub?name='+stub+"&route="+Store.routeOfInterest).then((response) => { 44 | return response.text() 45 | }).then((json) => { 46 | Store.stubs[stub] = json; 47 | Store.emit('STUB_GET_COMPLETE_EVENT'); 48 | }) 49 | }, 50 | postStubData: (state) => { 51 | fetch('/mocknode/api/modifystub', { 52 | method: 'post', 53 | headers: { 54 | 'Accept': 'application/json', 55 | 'Content-Type': 'application/json' 56 | }, 57 | body: JSON.stringify({ 58 | route: Store.routeOfInterest, 59 | oldname: state.oldName, 60 | name: state.name, 61 | description: state.description, 62 | content: state.content 63 | }) 64 | }).then((json) => { 65 | Store.getConfig(); 66 | }) 67 | }, 68 | postDynamicStubData: (state) => { 69 | fetch('/mocknode/api/modifydynamicstub', { 70 | method: 'post', 71 | headers: { 72 | 'Accept': 'application/json', 73 | 'Content-Type': 'application/json' 74 | }, 75 | body: JSON.stringify({ 76 | route: Store.routeOfInterest, 77 | oldname: state.oldName, 78 | name: state.name, 79 | description: state.description, 80 | defaultStub: state.defaultStub, 81 | conditions: state.conditions 82 | }) 83 | }).then((json) => { 84 | Store.getConfig(); 85 | }) 86 | }, 87 | deleteStub: (stub) => { 88 | fetch('/mocknode/api/deletestub?name='+stub+"&route="+Store.routeOfInterest).then((res) => { 89 | Store.getConfig(); 90 | Store.emit('STUBS_ACTIVATE_TAB_EVENT', 1); 91 | }) 92 | }, 93 | deleteDynamicStub: (stub) => { 94 | fetch('/mocknode/api/deletedynamicstub?name='+stub+"&route="+Store.routeOfInterest).then((res) => { 95 | Store.getConfig(); 96 | Store.emit('DYNAMIC_STUBS_ACTIVATE_TAB_EVENT', 1); 97 | }) 98 | }, 99 | deleteRoute: (route) => { 100 | fetch('/mocknode/api/deleteroute?route='+route).then((res) => { 101 | Store.getConfig(); 102 | }) 103 | } 104 | }); 105 | 106 | module.exports = Store; 107 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3000, 3 | "routes": [ 4 | { 5 | "route": "/users/", 6 | "handle": "proxy", 7 | "proxy": "http://localhost:8090", 8 | "stub": "users.json", 9 | "dynamicStub": "user conditions", 10 | "stubs": [ 11 | { 12 | "name": "admins.json", 13 | "description": "A list of admins, this returns an array." 14 | }, 15 | { 16 | "name": "users.json", 17 | "description": "this is the general users" 18 | } 19 | ], 20 | "dynamicStubs": [ 21 | { 22 | "name": "user conditions", 23 | "description": "this is an example conditional stubing", 24 | "defaultStub": "id.json", 25 | "conditions": [ 26 | { 27 | "eval": "req.body.user == 'admin'", 28 | "stub": "admins.json" 29 | }, 30 | { 31 | "eval": "req.query.name == 'anunay'", 32 | "stub": "users.json" 33 | } 34 | ] 35 | } 36 | ] 37 | }, 38 | { 39 | "route": "/id/", 40 | "handle": "dynamicStub", 41 | "proxy": "http://localhost:8090", 42 | "stub": "id.json", 43 | "dynamicStub": "random conditions", 44 | "stubs": [ 45 | { 46 | "name": "id.json", 47 | "description": "A list of ids" 48 | }, 49 | { 50 | "name": "newStub.json", 51 | "description": "dummy stub for example" 52 | } 53 | ], 54 | "dynamicStubs": [ 55 | { 56 | "name": "random conditions", 57 | "description": "this is conditional stubing for ids", 58 | "defaultStub": "id.json", 59 | "conditions": [ 60 | { 61 | "eval": "req.body.id == 'id'", 62 | "stub": "id.json" 63 | }, 64 | { 65 | "eval": "req.body.newStub == 'new'", 66 | "stub": "newStub.json" 67 | } 68 | ] 69 | } 70 | ] 71 | }, 72 | { 73 | "route": "/index/", 74 | "handle": "stub", 75 | "proxy": "http://localhost:8090", 76 | "stub": "index.xml", 77 | "dynamicStub": "", 78 | "stubs": [ 79 | { 80 | "name": "index.html", 81 | "description": "You can even stub html files" 82 | }, 83 | { 84 | "name": "index.xml", 85 | "description": "This is an example xml file" 86 | } 87 | ], 88 | "dynamicStubs": [] 89 | }, 90 | { 91 | "route": "/truthy/", 92 | "handle": "dynamicStub", 93 | "proxy": "http://localhost:3000", 94 | "stubs": [ 95 | { 96 | "name": "true.json", 97 | "description": "returns true :)" 98 | }, 99 | { 100 | "name": "false.json", 101 | "description": "returns false :)" 102 | }, 103 | { 104 | "name": "ambiguous.json", 105 | "description": "ambiguous" 106 | } 107 | ], 108 | "dynamicStubs": [ 109 | { 110 | "name": "dynamic stub", 111 | "description": "its a dynamic stub", 112 | "defaultStub": "ambiguous.json", 113 | "conditions": [ 114 | { 115 | "eval": "req.query.return == \"true\"", 116 | "stub": "true.json" 117 | }, 118 | { 119 | "eval": "req.query.return == \"false\"", 120 | "stub": "false.json" 121 | } 122 | ] 123 | }, 124 | { 125 | "name": "single dynamic stub", 126 | "description": "checking one condition", 127 | "defaultStub": "ambiguous.json", 128 | "conditions": [ 129 | { 130 | "eval": "req.query.return == \"true\"", 131 | "stub": "true.json" 132 | } 133 | ] 134 | }, 135 | { 136 | "name": "no dynamic", 137 | "description": "something goes here", 138 | "defaultStub": "ambiguous.json", 139 | "conditions": [] 140 | } 141 | ], 142 | "dynamicStub": "dynamic stub" 143 | } 144 | ], 145 | "global": { 146 | "delay": 0, 147 | "headers": { 148 | "Access-Control-Allow-Origin": "*", 149 | "X-Powered-By": "mocknode" 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mock Node 2 | A configurable mock server with an intuitive configuration management interface and a http api. 3 | 4 | [More Information...](https://medium.com/@i.anunay/mocknode-e338f793dba0) 5 | 6 | [![Build Status](https://travis-ci.org/ianunay/mock-node.svg?branch=master)](https://travis-ci.org/ianunay/mock-node) 7 | 8 | [![NPM](https://nodei.co/npm/mocknode.png?downloadRank=true)](https://www.npmjs.com/package/mocknode) 9 | 10 | Mocknode allows you to mock http endpoints quickly and easily. The simple management interface lets you configure how the server responds to different endpoints: 11 | 12 | 1. Proxy - Proxies the request to an existing service 13 | 2. Stub - A static response which you can manage in the 'manage stubs' link of the route 14 | 3. Dynamic Stub - Responds with a configured stub if the condition defined evaluates to true 15 | 16 | The interface allows every one working on the team to have clear visibility on what routes are being used by an application and all possible responses of the route. Thus making stubs to act as proper documented examples. 17 | 18 | You can use mocknode to write integration tests for your application. Dynamic stubs can be used to define strategies which can be asserted in your test scripts. You can have even more granular control on the integration tests by using the http api exposed by mocknode. Use the API to toggle the handle for a route, change the stub which is being responded, etc. 19 | 20 | ## A running instance on heroku 21 | 22 | [https://mocknode.herokuapp.com/mocknode/](https://mocknode.herokuapp.com/mocknode/) 23 | please go easy on this :) 24 | 25 | ## Installing and Runing the server 26 | 27 | ### Standalone 28 | 29 | 1. clone/download this repository 30 | 2. make sure you have node and npm installed 31 | 3. run npm install to download required packages 32 | 4. node server.js to run the server 33 | 34 | ### npm global install 35 | 36 | 1. npm install -g mocknode 37 | 2. run mocknode to start the server 38 | 39 | ### npm local install 40 | 41 | 1. npm install --save mocknode 42 | 2. add an npm script entry in the package.json "mocknode": "mocknode" 43 | 3. run npm run mocknode to start the server 44 | 45 | and open [https://localhost:3000/mocknode/](https://localhost:3000/mocknode/) in your browser to configure mocknode 46 | 47 | 48 | Mocknode has a downstream dependency on buffertools which executes a node-gyp during install. 49 | A common problem with node-gyp exists [nodejs/node-gyp#809](https://github.com/nodejs/node-gyp/issues/809). If there are node-gyp errors during 'npm install' an easy fix is to remove the gyp installed on your OS. 50 | 51 | ## The Port 52 | 53 | The order of preference for the port on which mocknode runs is: 54 | 55 | env variable of process > port option passed > 3000 56 | 57 | mocknode --port 4000 starts the server on port 4000, unless a PORT env variable exists for the process. 58 | 59 | 60 | ## Mocknode configuration 61 | 62 | Open [https://localhost:3000/mocknode/](https://localhost:3000/mocknode/) in your browser to configure mocknode 63 | 64 | ![alt tag](https://cloud.githubusercontent.com/assets/1129363/14989097/237e4478-114e-11e6-8083-b56cfa95dc4f.png) 65 | 66 | All your changes are saved in the config.json file and the stubs folder. This ensures all your changes are saved if you restart mocknode. You can easily backup all of your configuration by making a copy of these files. 67 | 68 | ## Installation directory 69 | 70 | If you have installed mocknode as a global npm package 71 | 72 | mocknode --location prints the installation path of mocknode. 73 | 74 | ## Logs 75 | 76 | mocknode stores logs in the logs/ folder of the installation directory 77 | 1. access.log - logs all requests to mocknode 78 | 2. change.log - logs all configuration change requests made to mocknode 79 | 80 | ## Export and Import config 81 | 82 | The config.json file and stubs folder hold all the configuration of mocknode. 83 | 84 | mocknode --export creates a mocknode-config.tar file which can be used to setup another instance of mocknode. 85 | mocknode --import [file_path] imports a config.tar file to configure mocknode. 86 | 87 | ## HTTP API 88 | 89 | Mocknode exposes a series of endpoints which can help you integrate it with your code - [ test scripts for example ] 90 | 91 | 92 | | enpoint | method | params | 93 | | :------------------------------ | --------:| :------------: | 94 | | /mocknode/api/config | GET | - | 95 | | /mocknode/api/stubconfig | GET | - | 96 | | /mocknode/api/getstub | GET | name | 97 | | /mocknode/api/modifyroute | POST | route config | 98 | | /mocknode/api/deleteroute | GET | route | 99 | | /mocknode/api/modifystub | POST | stub config | 100 | | /mocknode/api/deletestub | GET | name | 101 | | /mocknode/api/modifydynamicstub | POST | stub config | 102 | | /mocknode/api/deletedynamicstub | GET | name | 103 | 104 | 105 | mocknode interface uses the above endpoints to interact with the server, inspect the network of the browser to better understand the parameters used in each api. 106 | 107 | ### Troubleshooting 108 | 109 | The config files and the stubs folder mentioned above have all the information regarding your configuration. 110 | 111 | 112 | ### License 113 | 114 | MIT 115 | -------------------------------------------------------------------------------- /src/routingManager.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Store from 'store'; 3 | import {Input, ButtonToolbar, ButtonGroup, Button, Grid, Row, Col, Modal} from 'react-bootstrap'; 4 | import {isWebUri} from 'valid-url'; 5 | 6 | class RoutingManager extends React.Component { 7 | constructor(props, context) { 8 | super(props, context); 9 | this.handleChange = this.handleChange.bind(this); 10 | this.validate = this.validate.bind(this); 11 | this.updateRoute = this.updateRoute.bind(this); 12 | this.deleteRoute = this.deleteRoute.bind(this); 13 | this.manageStubs = this.manageStubs.bind(this); 14 | 15 | this.state = Object.assign( 16 | {formValid: false, old_route: this.props.route}, 17 | this.props, {handle: this.props.newRoute ? "proxy" : this.props.handle}, 18 | {proxy: this.props.newRoute ? window.location.origin : this.props.proxy} 19 | ); 20 | }; 21 | componentWillReceiveProps(nextProps){ 22 | this.setState({newRoute: nextProps.newRoute, old_route: nextProps.route}); 23 | } 24 | validate(state){ 25 | let validity = state.route && state.route.charAt(0) == '/' && state.route.charAt(state.route.length - 1) == '/' 26 | && state.route.length > 2 && !/[^a-zA-Z0-9\_\-\/]/.test( state.route ) && 27 | ((state.handle == "proxy" && state.proxy && isWebUri(state.proxy) && state.proxy.charAt(state.proxy.length - 1) != '/') 28 | || (state.handle == "stub" && state.stub) 29 | || (state.handle == "dynamicStub" && state.dynamicStub)); 30 | this.setState({formValid: validity}) 31 | } 32 | handleChange(elem, event){ 33 | let partialState = {}; 34 | partialState[elem] = event ? event.target.value : window.event.target.value; 35 | this.setState(partialState); 36 | this.validate(Object.assign(this.state, partialState)); 37 | } 38 | updateRoute(){ 39 | Store.changeConfig(this.state); 40 | } 41 | deleteRoute(){ 42 | if (confirm("Are you sure you want to delete the route : "+ (this.props.route || "new route") + " ? All the stubs related to this route will be deleted.")) { 43 | this.props.delete(); 44 | if (!this.props.newRoute) { 45 | Store.deleteRoute(this.props.route); 46 | } 47 | } 48 | } 49 | manageStubs(page, type) { 50 | Store.routeOfInterest = this.state.old_route; 51 | Store.updatePage(page); 52 | } 53 | render(){ 54 | let routeInput, 55 | stublist; 56 | if (this.state.handle == "proxy") { 57 | routeInput = ; 58 | } else if (this.state.handle == "stub") { 59 | let options = this.state.stubs.map((stub, i) => ); 60 | routeInput = ( 61 |
62 | 63 | 64 | {options} 65 | 66 | Manage stubs 67 |
68 | ); 69 | } else if (this.state.handle == "dynamicStub") { 70 | let options = this.state.dynamicStubs.map((stub, i) => ); 71 | routeInput = ( 72 |
73 | 74 | 75 | {options} 76 | 77 | Manage dynamic stubs 78 |
79 | ); 80 | } 81 | return ( 82 | 83 | 84 | 85 | Route link} placeholder="/route/" onChange={this.handleChange.bind(this, "route")} onBlur={this.handleChange.bind(this, "route")} /> 86 |
87 | {routeInput} 88 |
89 | 90 | 91 |
92 | 93 | 94 | 95 | 96 |
97 | 98 | 99 | 100 | 101 | 102 |
103 |
104 | ) 105 | } 106 | } 107 | 108 | module.exports = RoutingManager; 109 | -------------------------------------------------------------------------------- /src/stubForm-dynamic.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Store from 'store'; 3 | import {Input, Badge, Button, Grid, Row, Col, Modal} from 'react-bootstrap'; 4 | 5 | class StubForm extends React.Component { 6 | constructor(props, context) { 7 | super(props, context); 8 | this.handleChange = this.handleChange.bind(this); 9 | this.handleConditionChange = this.handleConditionChange.bind(this); 10 | this.validate = this.validate.bind(this); 11 | this.postData = this.postData.bind(this); 12 | this.deleteStub = this.deleteStub.bind(this); 13 | this.addConditions = this.addConditions.bind(this); 14 | this.removeCondition = this.removeCondition.bind(this); 15 | 16 | this.state = Object.assign({oldName: this.props.name, stublist: [], conditions: []}, this.props); 17 | }; 18 | componentWillReceiveProps(nextProps){ 19 | this.setState({stublist: nextProps.stublist}); 20 | } 21 | handleChange(elem, event){ 22 | let partialState = {}; 23 | partialState[elem] = event ? event.target.value : window.event.target.value; 24 | this.setState(partialState); 25 | this.validate(Object.assign(this.state, partialState)); 26 | } 27 | handleConditionChange(elem, index, event){ 28 | let newConditions = this.state.conditions.slice(); 29 | let partialCondition = newConditions[index]; 30 | 31 | partialCondition[elem] = event ? event.target.value : window.event.target.value; 32 | this.setState({conditions: newConditions}); 33 | this.validate(Object.assign(this.state, {conditions: newConditions})); 34 | } 35 | postData(){ 36 | Store.postDynamicStubData(this.state); 37 | } 38 | validate(state){ 39 | let valid = state.name && state.defaultStub; 40 | valid && state.conditions.map(function(condition) { 41 | if (!condition.eval || !condition.stub) 42 | valid = false; 43 | }); 44 | this.setState({formValid: valid}); 45 | } 46 | deleteStub(){ 47 | if (confirm("Are you sure you want to delete the stub : "+ this.props.name + " ?")) 48 | Store.deleteDynamicStub(this.props.name); 49 | } 50 | addConditions(){ 51 | let newState = Object.assign({}, this.state); 52 | newState.conditions.push({}); 53 | this.setState(newState); 54 | this.validate(newState); 55 | } 56 | removeCondition(index){ 57 | let newConditions = this.state.conditions.slice(); 58 | newConditions.splice(index, 1); 59 | this.setState({conditions: newConditions}); 60 | this.validate(Object.assign({}, this.state, {conditions: newConditions})); 61 | } 62 | render(){ 63 | let actions = this.state.new 64 | ?
65 | : (
66 |
); 67 | 68 | let options = this.state.stublist.map((stub, i) => ); 69 | 70 | let conditions = this.state.conditions.map((condition, i) => ( 71 | 72 | {i+1} 73 | 74 | use req to form a javascript expression this.setState({helpModal: true})}>help on conditions

}/> 77 | 78 | 79 | {options} 80 | 81 | Remove Condition 82 | 83 |
84 | ) 85 | ); 86 | 87 | return ( 88 |
89 |
90 | 92 | 94 | 95 | 96 | {options} 97 | 98 |

Conditions are javascript expressions that can leverage the req object.

99 |

Each condition is run sequentially in a sandbox environment and the stub corrensponding to the first matched condition is choosen as the response.

100 |

If none of these conditions evaluate to a javascript true, then the Default Stub is responded.

101 | 102 | 103 | {conditions} 104 | 105 | Add Conditions 106 | {actions} 107 |
108 | 109 | 110 | Help on conditions 111 | 112 | 113 |

The req is a javascript object which you can use to form conditions

114 |

If the incoming request has query strings use req.query.key to read the value.

115 |

If the incoming request is a post use req.body.key to read the value.

116 |

Use req.path == '/path' to match a url which ends with '/path'

117 |

List of all properties available in the req object :

118 |
baseURL
body
cookies
headers
hostname
ip
ips
method
originalUrl
params
path
protocol
query
route
signedCookies
stale
subdomains
xhr
119 |

These properties are being provided by expressjs module

120 |

See more about these properties here

121 |
122 | 123 | 124 | 125 |
126 |
127 | ) 128 | } 129 | } 130 | 131 | module.exports = StubForm; 132 | -------------------------------------------------------------------------------- /dist/script/fetch.js: -------------------------------------------------------------------------------- 1 | (function(self) { 2 | 'use strict'; 3 | 4 | if (self.fetch) { 5 | return 6 | } 7 | 8 | function normalizeName(name) { 9 | if (typeof name !== 'string') { 10 | name = String(name) 11 | } 12 | if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) { 13 | throw new TypeError('Invalid character in header field name') 14 | } 15 | return name.toLowerCase() 16 | } 17 | 18 | function normalizeValue(value) { 19 | if (typeof value !== 'string') { 20 | value = String(value) 21 | } 22 | return value 23 | } 24 | 25 | function Headers(headers) { 26 | this.map = {} 27 | 28 | if (headers instanceof Headers) { 29 | headers.forEach(function(value, name) { 30 | this.append(name, value) 31 | }, this) 32 | 33 | } else if (headers) { 34 | Object.getOwnPropertyNames(headers).forEach(function(name) { 35 | this.append(name, headers[name]) 36 | }, this) 37 | } 38 | } 39 | 40 | Headers.prototype.append = function(name, value) { 41 | name = normalizeName(name) 42 | value = normalizeValue(value) 43 | var list = this.map[name] 44 | if (!list) { 45 | list = [] 46 | this.map[name] = list 47 | } 48 | list.push(value) 49 | } 50 | 51 | Headers.prototype['delete'] = function(name) { 52 | delete this.map[normalizeName(name)] 53 | } 54 | 55 | Headers.prototype.get = function(name) { 56 | var values = this.map[normalizeName(name)] 57 | return values ? values[0] : null 58 | } 59 | 60 | Headers.prototype.getAll = function(name) { 61 | return this.map[normalizeName(name)] || [] 62 | } 63 | 64 | Headers.prototype.has = function(name) { 65 | return this.map.hasOwnProperty(normalizeName(name)) 66 | } 67 | 68 | Headers.prototype.set = function(name, value) { 69 | this.map[normalizeName(name)] = [normalizeValue(value)] 70 | } 71 | 72 | Headers.prototype.forEach = function(callback, thisArg) { 73 | Object.getOwnPropertyNames(this.map).forEach(function(name) { 74 | this.map[name].forEach(function(value) { 75 | callback.call(thisArg, value, name, this) 76 | }, this) 77 | }, this) 78 | } 79 | 80 | function consumed(body) { 81 | if (body.bodyUsed) { 82 | return Promise.reject(new TypeError('Already read')) 83 | } 84 | body.bodyUsed = true 85 | } 86 | 87 | function fileReaderReady(reader) { 88 | return new Promise(function(resolve, reject) { 89 | reader.onload = function() { 90 | resolve(reader.result) 91 | } 92 | reader.onerror = function() { 93 | reject(reader.error) 94 | } 95 | }) 96 | } 97 | 98 | function readBlobAsArrayBuffer(blob) { 99 | var reader = new FileReader() 100 | reader.readAsArrayBuffer(blob) 101 | return fileReaderReady(reader) 102 | } 103 | 104 | function readBlobAsText(blob) { 105 | var reader = new FileReader() 106 | reader.readAsText(blob) 107 | return fileReaderReady(reader) 108 | } 109 | 110 | var support = { 111 | blob: 'FileReader' in self && 'Blob' in self && (function() { 112 | try { 113 | new Blob(); 114 | return true 115 | } catch(e) { 116 | return false 117 | } 118 | })(), 119 | formData: 'FormData' in self, 120 | arrayBuffer: 'ArrayBuffer' in self 121 | } 122 | 123 | function Body() { 124 | this.bodyUsed = false 125 | 126 | 127 | this._initBody = function(body) { 128 | this._bodyInit = body 129 | if (typeof body === 'string') { 130 | this._bodyText = body 131 | } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { 132 | this._bodyBlob = body 133 | } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { 134 | this._bodyFormData = body 135 | } else if (!body) { 136 | this._bodyText = '' 137 | } else if (support.arrayBuffer && ArrayBuffer.prototype.isPrototypeOf(body)) { 138 | // Only support ArrayBuffers for POST method. 139 | // Receiving ArrayBuffers happens via Blobs, instead. 140 | } else { 141 | throw new Error('unsupported BodyInit type') 142 | } 143 | 144 | if (!this.headers.get('content-type')) { 145 | if (typeof body === 'string') { 146 | this.headers.set('content-type', 'text/plain;charset=UTF-8') 147 | } else if (this._bodyBlob && this._bodyBlob.type) { 148 | this.headers.set('content-type', this._bodyBlob.type) 149 | } 150 | } 151 | } 152 | 153 | if (support.blob) { 154 | this.blob = function() { 155 | var rejected = consumed(this) 156 | if (rejected) { 157 | return rejected 158 | } 159 | 160 | if (this._bodyBlob) { 161 | return Promise.resolve(this._bodyBlob) 162 | } else if (this._bodyFormData) { 163 | throw new Error('could not read FormData body as blob') 164 | } else { 165 | return Promise.resolve(new Blob([this._bodyText])) 166 | } 167 | } 168 | 169 | this.arrayBuffer = function() { 170 | return this.blob().then(readBlobAsArrayBuffer) 171 | } 172 | 173 | this.text = function() { 174 | var rejected = consumed(this) 175 | if (rejected) { 176 | return rejected 177 | } 178 | 179 | if (this._bodyBlob) { 180 | return readBlobAsText(this._bodyBlob) 181 | } else if (this._bodyFormData) { 182 | throw new Error('could not read FormData body as text') 183 | } else { 184 | return Promise.resolve(this._bodyText) 185 | } 186 | } 187 | } else { 188 | this.text = function() { 189 | var rejected = consumed(this) 190 | return rejected ? rejected : Promise.resolve(this._bodyText) 191 | } 192 | } 193 | 194 | if (support.formData) { 195 | this.formData = function() { 196 | return this.text().then(decode) 197 | } 198 | } 199 | 200 | this.json = function() { 201 | return this.text().then(JSON.parse) 202 | } 203 | 204 | return this 205 | } 206 | 207 | // HTTP methods whose capitalization should be normalized 208 | var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] 209 | 210 | function normalizeMethod(method) { 211 | var upcased = method.toUpperCase() 212 | return (methods.indexOf(upcased) > -1) ? upcased : method 213 | } 214 | 215 | function Request(input, options) { 216 | options = options || {} 217 | var body = options.body 218 | if (Request.prototype.isPrototypeOf(input)) { 219 | if (input.bodyUsed) { 220 | throw new TypeError('Already read') 221 | } 222 | this.url = input.url 223 | this.credentials = input.credentials 224 | if (!options.headers) { 225 | this.headers = new Headers(input.headers) 226 | } 227 | this.method = input.method 228 | this.mode = input.mode 229 | if (!body) { 230 | body = input._bodyInit 231 | input.bodyUsed = true 232 | } 233 | } else { 234 | this.url = input 235 | } 236 | 237 | this.credentials = options.credentials || this.credentials || 'omit' 238 | if (options.headers || !this.headers) { 239 | this.headers = new Headers(options.headers) 240 | } 241 | this.method = normalizeMethod(options.method || this.method || 'GET') 242 | this.mode = options.mode || this.mode || null 243 | this.referrer = null 244 | 245 | if ((this.method === 'GET' || this.method === 'HEAD') && body) { 246 | throw new TypeError('Body not allowed for GET or HEAD requests') 247 | } 248 | this._initBody(body) 249 | } 250 | 251 | Request.prototype.clone = function() { 252 | return new Request(this) 253 | } 254 | 255 | function decode(body) { 256 | var form = new FormData() 257 | body.trim().split('&').forEach(function(bytes) { 258 | if (bytes) { 259 | var split = bytes.split('=') 260 | var name = split.shift().replace(/\+/g, ' ') 261 | var value = split.join('=').replace(/\+/g, ' ') 262 | form.append(decodeURIComponent(name), decodeURIComponent(value)) 263 | } 264 | }) 265 | return form 266 | } 267 | 268 | function headers(xhr) { 269 | var head = new Headers() 270 | var pairs = xhr.getAllResponseHeaders().trim().split('\n') 271 | pairs.forEach(function(header) { 272 | var split = header.trim().split(':') 273 | var key = split.shift().trim() 274 | var value = split.join(':').trim() 275 | head.append(key, value) 276 | }) 277 | return head 278 | } 279 | 280 | Body.call(Request.prototype) 281 | 282 | function Response(bodyInit, options) { 283 | if (!options) { 284 | options = {} 285 | } 286 | 287 | this.type = 'default' 288 | this.status = options.status 289 | this.ok = this.status >= 200 && this.status < 300 290 | this.statusText = options.statusText 291 | this.headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers) 292 | this.url = options.url || '' 293 | this._initBody(bodyInit) 294 | } 295 | 296 | Body.call(Response.prototype) 297 | 298 | Response.prototype.clone = function() { 299 | return new Response(this._bodyInit, { 300 | status: this.status, 301 | statusText: this.statusText, 302 | headers: new Headers(this.headers), 303 | url: this.url 304 | }) 305 | } 306 | 307 | Response.error = function() { 308 | var response = new Response(null, {status: 0, statusText: ''}) 309 | response.type = 'error' 310 | return response 311 | } 312 | 313 | var redirectStatuses = [301, 302, 303, 307, 308] 314 | 315 | Response.redirect = function(url, status) { 316 | if (redirectStatuses.indexOf(status) === -1) { 317 | throw new RangeError('Invalid status code') 318 | } 319 | 320 | return new Response(null, {status: status, headers: {location: url}}) 321 | } 322 | 323 | self.Headers = Headers; 324 | self.Request = Request; 325 | self.Response = Response; 326 | 327 | self.fetch = function(input, init) { 328 | return new Promise(function(resolve, reject) { 329 | var request 330 | if (Request.prototype.isPrototypeOf(input) && !init) { 331 | request = input 332 | } else { 333 | request = new Request(input, init) 334 | } 335 | 336 | var xhr = new XMLHttpRequest() 337 | 338 | function responseURL() { 339 | if ('responseURL' in xhr) { 340 | return xhr.responseURL 341 | } 342 | 343 | // Avoid security warnings on getResponseHeader when not allowed by CORS 344 | if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) { 345 | return xhr.getResponseHeader('X-Request-URL') 346 | } 347 | 348 | return; 349 | } 350 | 351 | xhr.onload = function() { 352 | var status = (xhr.status === 1223) ? 204 : xhr.status 353 | if (status < 100 || status > 599) { 354 | reject(new TypeError('Network request failed')) 355 | return 356 | } 357 | var options = { 358 | status: status, 359 | statusText: xhr.statusText, 360 | headers: headers(xhr), 361 | url: responseURL() 362 | } 363 | var body = 'response' in xhr ? xhr.response : xhr.responseText; 364 | resolve(new Response(body, options)) 365 | } 366 | 367 | xhr.onerror = function() { 368 | reject(new TypeError('Network request failed')) 369 | } 370 | 371 | xhr.open(request.method, request.url, true) 372 | 373 | if (request.credentials === 'include') { 374 | xhr.withCredentials = true 375 | } 376 | 377 | if ('responseType' in xhr && support.blob) { 378 | xhr.responseType = 'blob' 379 | } 380 | 381 | request.headers.forEach(function(value, name) { 382 | xhr.setRequestHeader(name, value) 383 | }) 384 | 385 | xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) 386 | }) 387 | } 388 | self.fetch.polyfill = true 389 | })(typeof self !== 'undefined' ? self : this); -------------------------------------------------------------------------------- /src/server.es6: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const configFile = __dirname + '/config.json', 4 | interfaceFolder = __dirname + '/dist'; 5 | 6 | let express = require('express'), 7 | app = express(), 8 | router = express.Router(), 9 | proxy = require('express-http-proxy'), 10 | Url = require('url'), 11 | fs = require('fs'), 12 | path = require('path'), 13 | bodyParser = require('body-parser'), 14 | cookieParser = require('cookie-parser'), 15 | rimraf = require('rimraf'), 16 | winston = require('winston'), 17 | SandCastle = require('sandcastle').SandCastle, 18 | sandcastle = new SandCastle(), 19 | async = require('async'), 20 | config = require(configFile); 21 | 22 | let argv = require('minimist')(process.argv.slice(2)); 23 | 24 | if (argv.location) 25 | console.log('mocknode installation directory: ', __dirname); 26 | else if (argv.export) { 27 | let fse = require('fs-extra'), 28 | tmp_path = path.join(__dirname, 'tmp'), 29 | tar = require('tar-fs'); 30 | 31 | fse.emptyDirSync(tmp_path); 32 | fse.copySync(__dirname + '/stubs', tmp_path + '/stubs'); 33 | fse.copySync(__dirname + '/config.json', tmp_path + '/config.json'); 34 | tar.pack(tmp_path).pipe(fs.createWriteStream('mocknode-config.tar')); 35 | console.log('mocknode config has been exported to mocknode-config.tar'); 36 | } else if (argv.import) { 37 | let tar = require('tar-fs'); 38 | fs.createReadStream(argv.import).pipe(tar.extract(__dirname)); 39 | console.log('configuration has been imported'); 40 | } else { 41 | 42 | // App starts 43 | const port = process.env.PORT || argv.port || config.port; 44 | 45 | // Support for v0.x 46 | let assign = require('object-assign'); 47 | 48 | app.use(bodyParser.urlencoded({extended: false})); 49 | app.use(bodyParser.json()); 50 | app.use(cookieParser()) 51 | app.use(router); 52 | 53 | let sleep = (milliseconds) => { 54 | var start = new Date().getTime(); 55 | for (var i = 0; i < 1e7; i++) { 56 | if ((new Date().getTime() - start) > milliseconds){ 57 | break; 58 | } 59 | } 60 | } 61 | 62 | let logger = { 63 | changelog: new (winston.Logger)({ 64 | transports: [ 65 | new (winston.transports.File)({ 66 | name: 'changelog', 67 | filename: path.join(__dirname , 'logs', 'change.log') 68 | }) 69 | ] 70 | }), 71 | accesslog: new (winston.Logger)({ 72 | transports: [ 73 | new (winston.transports.File)({ 74 | name: 'accesslog', 75 | filename: path.join(__dirname , 'logs', 'access.log') 76 | }) 77 | ] 78 | }) 79 | } 80 | 81 | // Global headers and gloabl delay 82 | // currently this is only read from the config file 83 | // TODO: buid and interface for this. 84 | let globalHeaders = (globalConfig) => ((req, res, next) => { 85 | Object.keys(globalConfig.headers).map((header) => { 86 | res.setHeader(header, globalConfig.headers[header]); 87 | }); 88 | if (globalConfig.delay) 89 | sleep(globalConfig.delay); 90 | return next(); 91 | }); 92 | 93 | // Returns a middleware which proxies to the target 94 | // TODO: exception handling for targets which are wrong, 95 | // currently the proxy fails and breaks the server. 96 | let assignNewProxy = (target) => ( 97 | proxy(target, { 98 | forwardPath: (req, res) => Url.parse(req.originalUrl).path 99 | }) 100 | ); 101 | 102 | // Filesystems dont allow '/' in the names of folders / files, 103 | // Converting this character to a '!' 104 | let encodeRoutePath = (route) => route.replace(/\//g, "!"); 105 | let decodeRoutePath = (route) => route.replace(/!/g, "/"); 106 | 107 | 108 | // Creators and assigners - subtle abstraction : These return middlewares 109 | let createProxyRoute = (route, target) => router.use(route, assignNewProxy(target)); 110 | 111 | let createStubRoute = (route, stub) => router.use(route, stubHandler(route, stub)); 112 | 113 | let createDynamicStubRoute = (route, dynamicStub) => router.use(route, dynamicStubHandler(route, dynamicStub)); 114 | 115 | 116 | let dynamicStubHandler = (_route, _name) => { 117 | let dynamicObj; 118 | configLoop: 119 | for (let i = 0; i < config.routes.length; i++) { 120 | if (config.routes[i].route == _route) { 121 | for (var j = 0; j < config.routes[i].dynamicStubs.length; j++) { 122 | if (config.routes[i].dynamicStubs[j].name == _name) { 123 | dynamicObj = config.routes[i].dynamicStubs[j]; 124 | break configLoop; 125 | } 126 | }; 127 | }; 128 | } 129 | return dynamicStubRequestHandler(_route, dynamicObj); 130 | } 131 | 132 | // Middleware which returns a stub 133 | let stubHandler = (route, stub) => (req, res, next) => res.sendFile(path.join(__dirname, 'stubs', encodeRoutePath(route), stub)); 134 | 135 | // Middleware which handles the dynamic stubs conditions 136 | // Uses sandcastle to execute evals on a node sanbox. 137 | // The req object parameters are injected into the execution 138 | // runtime, so the eval expressions can access the req object. 139 | let dynamicStubRequestHandler = (_route, _stub) => { 140 | return (req, res) => { 141 | let returnedStub = _stub.defaultStub, 142 | continueLoop = true, 143 | count = 0; 144 | 145 | if (_stub.conditions.length) { 146 | async.whilst( 147 | () => (count < _stub.conditions.length) && continueLoop, 148 | (callback) => { 149 | let script = sandcastle.createScript(`exports.main = function() { 150 | try { 151 | if (${_stub.conditions[count].eval}) 152 | exit('${_stub.conditions[count].stub}') 153 | else 154 | exit(false) 155 | } catch(e) { 156 | exit(false) 157 | } 158 | }`); 159 | 160 | count++; 161 | 162 | script.on('exit', function(err, output) { 163 | if (output) { 164 | continueLoop = false 165 | returnedStub = output 166 | } 167 | callback(); 168 | }); 169 | 170 | script.run({ 171 | req: assign({}, { 172 | baseURL: req.baseURL, 173 | body: req.body, 174 | cookies: req.cookies, 175 | headers: req.headers, 176 | hostname: req.hostname, 177 | ip: req.ip, 178 | ips: req.ips, 179 | method: req.method, 180 | originalUrl: req.originalUrl, 181 | params: req.params, 182 | path: req.path, 183 | protocol: req.protocol, 184 | query: req.query, 185 | route: req.route, 186 | signedCookies: req.signedCookies, 187 | stale: req.stale, 188 | subdomains: req.subdomains, 189 | xhr: req.xhr 190 | }) 191 | }); 192 | }, 193 | (err) => { 194 | res.sendFile(path.join(__dirname, 'stubs', encodeRoutePath(_route), returnedStub)); 195 | } 196 | ); 197 | } 198 | else { 199 | res.sendFile(path.join(__dirname, 'stubs', encodeRoutePath(_route), returnedStub)); 200 | } 201 | } 202 | } 203 | 204 | // Create and update a route 205 | // TODO: RE-Implement this and make it pretty 206 | let updateRoute = (_req) => { 207 | let matchCount = 0; 208 | if (_req.old_route != _req.route) { 209 | if (_req.old_route) 210 | deleteroute(_req.old_route); 211 | delete _req.old_route; 212 | } else { 213 | delete _req.old_route; 214 | for (let layer of router.stack) { 215 | let match = _req.route.match(layer.regexp); 216 | if (match && match[0] == _req.route) { 217 | layer.handle = (_req.handle == "stub") 218 | ? stubHandler(_req.route, _req.stub) 219 | : (_req.handle == "proxy") 220 | ? assignNewProxy(_req.proxy) 221 | : dynamicStubHandler(_req.route, _req.dynamicStub); 222 | for (var i = 0; i < config.routes.length; i++) { 223 | if (config.routes[i].route == _req.route) { 224 | config.routes[i] = assign({}, config.routes[i], _req); 225 | } 226 | } 227 | matchCount++; 228 | } 229 | } 230 | } 231 | if (matchCount == 0) { 232 | if (_req.handle == "stub") { 233 | createStubRoute(_req.route, _req.stub); 234 | } else if (_req.handle == "proxy") { 235 | createProxyRoute(_req.route, _req.proxy); 236 | } else if (_req.handle == "dynamicStub") { 237 | createDynamicStubRoute(_req.route, _req.dynamicStub); 238 | } 239 | config.routes.push(assign({}, _req, {stubs:[], dynamicStubs: []})); 240 | let dir = path.join(__dirname, 'stubs', encodeRoutePath(_req.route)); 241 | if (!fs.existsSync(dir)) 242 | fs.mkdirSync(dir) 243 | } 244 | fs.writeFile(configFile, JSON.stringify(config, null, 2)); 245 | } 246 | 247 | // Create and update for stubs 248 | let updateStubs = (_req) => { 249 | let matchCount = 0; 250 | routeLoop: 251 | for (var i = 0; i < config.routes.length; i++) { 252 | if (config.routes[i].route == _req.route) { 253 | for (var j = 0; j < config.routes[i].stubs.length; j++) { 254 | if(config.routes[i].stubs[j].name == _req.oldname || config.routes[i].stubs[j].name == _req.name) { 255 | config.routes[i].stubs[j].name = _req.name; 256 | config.routes[i].stubs[j].description = _req.description; 257 | fs.writeFileSync(path.join(__dirname, 'stubs', encodeRoutePath(_req.route), _req.name), _req.content); 258 | if (_req.oldname) { 259 | fs.rename( 260 | path.join(__dirname, 'stubs', encodeRoutePath(_req.route), _req.oldname), 261 | path.join(__dirname, 'stubs', encodeRoutePath(_req.route), _req.name) 262 | ) 263 | } 264 | matchCount++; 265 | break routeLoop; 266 | } 267 | } 268 | if (matchCount == 0) { 269 | fs.writeFile(path.join(__dirname, 'stubs', encodeRoutePath(_req.route), _req.name), _req.content); 270 | config.routes[i].stubs.push({name: _req.name, description: _req.description}); 271 | } 272 | } 273 | } 274 | fs.writeFile(configFile, JSON.stringify(config, null, 2)); 275 | } 276 | 277 | // Create and update for dynamic stubs, this is very similar 278 | // to that of stubs but there is no file associated, only the 279 | // config is updated. 280 | let updateDynamicStubs = (_req) => { 281 | let req = assign({}, _req); 282 | let matchCount = 0, 283 | oldname = req.oldname, 284 | route = req.route; 285 | delete req.oldname; 286 | delete req.route; 287 | routeLoop: 288 | for (var i = 0; i < config.routes.length; i++) { 289 | if (config.routes[i].route == route) { 290 | for (var j = 0; j < config.routes[i].dynamicStubs.length; j++) { 291 | if(config.routes[i].dynamicStubs[j].name == oldname || config.routes[i].dynamicStubs[j].name == req.name) { 292 | config.routes[i].dynamicStubs[j] = req; 293 | matchCount++; 294 | break routeLoop; 295 | } 296 | } 297 | if (matchCount == 0) { 298 | config.routes[i].dynamicStubs.push(req); 299 | } 300 | } 301 | } 302 | fs.writeFile(configFile, JSON.stringify(config, null, 2)); 303 | } 304 | 305 | // Delete a route: 306 | // 1. Remove it from the router stack 307 | // 2. Remote it from config 308 | // 3. Delete the stubs/route folder 309 | let deleteroute = (_route) => { 310 | let index = router.stack.map((layer) => _route.match(layer.regexp)) 311 | .reduce((index, item, i) => !!item ? i : index, 0); 312 | if (index > -1) { 313 | router.stack.splice(index, 1); 314 | rimraf(path.join(__dirname, 'stubs', encodeRoutePath(_route)), () => {}); 315 | } 316 | let newRoutes = config.routes.filter((route) => route.route != _route); 317 | config = assign(config, {routes: newRoutes}); 318 | fs.writeFile(configFile, JSON.stringify(config, null, 2)); 319 | } 320 | 321 | // Delete a stub: 322 | // 1. Delete the file 323 | // 2. Remove the entry from config file 324 | let deletestub = (_route, _stub) => { 325 | fs.unlinkSync(path.join(__dirname, 'stubs', encodeRoutePath(_route), _stub)); 326 | 327 | routeLoop: 328 | for (var i = 0; i < config.routes.length; i++) { 329 | if (config.routes[i].route == _route) { 330 | for (var j = 0; j < config.routes[i].stubs.length; j++) { 331 | if(config.routes[i].stubs[j].name == _stub) { 332 | config.routes[i].stubs.splice(j, 1); 333 | break routeLoop; 334 | } 335 | } 336 | } 337 | } 338 | fs.writeFile(configFile, JSON.stringify(config, null, 2)); 339 | } 340 | 341 | // Delete a dynamic stub: 342 | // 1. Remove the entry from config file 343 | let deleteDynamicstub = (_route, _stub) => { 344 | 345 | routeLoop: 346 | for (var i = 0; i < config.routes.length; i++) { 347 | if (config.routes[i].route == _route) { 348 | for (var j = 0; j < config.routes[i].dynamicStubs.length; j++) { 349 | if(config.routes[i].dynamicStubs[j].name == _stub) { 350 | config.routes[i].dynamicStubs.splice(j, 1); 351 | break routeLoop; 352 | } 353 | } 354 | } 355 | } 356 | fs.writeFile(configFile, JSON.stringify(config, null, 2)); 357 | } 358 | 359 | // Expects that the dynamic stub properties are updated 360 | // checks if the route is using this dynamic stub and updates its layer.handle 361 | let updateDynamicRoutes = (_route, _dynamicStub) => { 362 | for (var i = 0; i < config.routes.length; i++) { 363 | if ( (_route == config.routes[i].route) 364 | && (_dynamicStub == config.routes[i].dynamicStub) 365 | && ("dynamicStub" == config.routes[i].handle) ) { 366 | 367 | for (let layer of router.stack) { 368 | let match = config.routes[i].route.match(layer.regexp); 369 | if (match && match[0] == config.routes[i].route) { 370 | layer.handle = dynamicStubHandler(_route, _dynamicStub); 371 | } 372 | } 373 | 374 | } 375 | } 376 | } 377 | 378 | let logRequest = (_req, _type, _method) => { 379 | logger[_type][_method]({ 380 | 'route': _req.path, 381 | 'query_strings': JSON.stringify(_req.query), 382 | 'request_body': JSON.stringify(_req.body), 383 | 'ip': _req.ip 384 | }); 385 | } 386 | 387 | // Logs all requests which do not start with '/mocknode/' 388 | router.use('/', (req, res, next) => { 389 | if (/^(?!\/mocknode\/)/.test(req.originalUrl) && /^(?!\/favicon.ico)/.test(req.originalUrl)) 390 | logRequest(req, 'accesslog', 'info'); 391 | next(); 392 | }); 393 | 394 | // Logs requests that change the configuration of mocknode 395 | router.use('/mocknode/api', (req, res, next) => { 396 | let logList = [ '/modifyroute', '/deleteroute', '/modifystub', '/deletestub', 397 | '/modifydynamicstub', '/deletedynamicstub']; 398 | if (logList.indexOf(req.path) > -1) 399 | logRequest(req, 'changelog', 'info'); 400 | next(); 401 | }); 402 | 403 | 404 | router.use('/mocknode', express.static(interfaceFolder)); 405 | router.use('/mocknode/api/config', (req, res) => res.json(config)); 406 | 407 | router.use('/mocknode/api/stubconfig', (req, res) => res.json(stubConfig)); 408 | 409 | router.use('/mocknode/api/logs', (req, res) => { 410 | res.sendFile(path.join(__dirname, 'logs', req.query.name)); 411 | }) 412 | 413 | router.use('/mocknode/api/getstub', (req, res) => { 414 | res.sendFile(path.join(__dirname, 'stubs', encodeRoutePath(req.query.route), req.query.name)); 415 | }); 416 | 417 | router.use('/mocknode/api/modifyroute', (req, res, next) => { 418 | updateRoute(req.body); 419 | res.send({success: true}); 420 | }); 421 | 422 | router.use('/mocknode/api/deleteroute', (req, res, next) => { 423 | deleteroute(req.query.route); 424 | res.send({success: true}); 425 | }); 426 | 427 | router.use('/mocknode/api/modifystub', (req, res) => { 428 | updateStubs(req.body); 429 | res.send({success: true}); 430 | }); 431 | 432 | router.use('/mocknode/api/deletestub', (req, res) => { 433 | deletestub(req.query.route, req.query.name); 434 | res.send({success: true}); 435 | }); 436 | 437 | router.use('/mocknode/api/modifydynamicstub', (req, res) => { 438 | updateDynamicStubs(req.body); 439 | updateDynamicRoutes(req.body.route, req.body.name); 440 | res.send({success: true}); 441 | }); 442 | 443 | router.use('/mocknode/api/deletedynamicstub', (req, res) => { 444 | deleteDynamicstub(req.query.route, req.query.name); 445 | res.send({success: true}); 446 | }); 447 | 448 | router.use(globalHeaders(config.global)); 449 | 450 | config.routes.filter((configObj) => configObj.handle == "proxy") 451 | .map((configObj) => createProxyRoute(configObj.route, configObj.proxy)); 452 | 453 | config.routes.filter((configObj) => configObj.handle == "stub") 454 | .map((configObj) => createStubRoute(configObj.route, configObj.stub)); 455 | 456 | config.routes.filter((configObj) => configObj.handle == "dynamicStub") 457 | .map((configObj) => createDynamicStubRoute(configObj.route, configObj.dynamicStub)); 458 | 459 | app.listen( port ); 460 | console.log( "Mocknode started on port: " + port ); 461 | console.log( "open 'http://localhost:" + port + "/mocknode' in your browser to configure mocknode" ); 462 | 463 | // App ends 464 | } 465 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var configFile = __dirname + '/config.json', 4 | interfaceFolder = __dirname + '/dist'; 5 | 6 | var express = require('express'), 7 | app = express(), 8 | router = express.Router(), 9 | proxy = require('express-http-proxy'), 10 | Url = require('url'), 11 | fs = require('fs'), 12 | path = require('path'), 13 | bodyParser = require('body-parser'), 14 | cookieParser = require('cookie-parser'), 15 | rimraf = require('rimraf'), 16 | winston = require('winston'), 17 | SandCastle = require('sandcastle').SandCastle, 18 | sandcastle = new SandCastle(), 19 | async = require('async'), 20 | config = require(configFile); 21 | 22 | var argv = require('minimist')(process.argv.slice(2)); 23 | 24 | if (argv.location) console.log('mocknode installation directory: ', __dirname);else if (argv.export) { 25 | var fse = require('fs-extra'), 26 | tmp_path = path.join(__dirname, 'tmp'), 27 | tar = require('tar-fs'); 28 | 29 | fse.emptyDirSync(tmp_path); 30 | fse.copySync(__dirname + '/stubs', tmp_path + '/stubs'); 31 | fse.copySync(__dirname + '/config.json', tmp_path + '/config.json'); 32 | tar.pack(tmp_path).pipe(fs.createWriteStream('mocknode-config.tar')); 33 | console.log('mocknode config has been exported to mocknode-config.tar'); 34 | } else if (argv.import) { 35 | var _tar = require('tar-fs'); 36 | fs.createReadStream(argv.import).pipe(_tar.extract(__dirname)); 37 | console.log('configuration has been imported'); 38 | } else { 39 | (function () { 40 | 41 | // App starts 42 | var port = process.env.PORT || argv.port || config.port; 43 | 44 | // Support for v0.x 45 | var assign = require('object-assign'); 46 | 47 | app.use(bodyParser.urlencoded({ extended: false })); 48 | app.use(bodyParser.json()); 49 | app.use(cookieParser()); 50 | app.use(router); 51 | 52 | var sleep = function sleep(milliseconds) { 53 | var start = new Date().getTime(); 54 | for (var i = 0; i < 1e7; i++) { 55 | if (new Date().getTime() - start > milliseconds) { 56 | break; 57 | } 58 | } 59 | }; 60 | 61 | var logger = { 62 | changelog: new winston.Logger({ 63 | transports: [new winston.transports.File({ 64 | name: 'changelog', 65 | filename: path.join(__dirname, 'logs', 'change.log') 66 | })] 67 | }), 68 | accesslog: new winston.Logger({ 69 | transports: [new winston.transports.File({ 70 | name: 'accesslog', 71 | filename: path.join(__dirname, 'logs', 'access.log') 72 | })] 73 | }) 74 | }; 75 | 76 | // Global headers and gloabl delay 77 | // currently this is only read from the config file 78 | // TODO: buid and interface for this. 79 | var globalHeaders = function globalHeaders(globalConfig) { 80 | return function (req, res, next) { 81 | Object.keys(globalConfig.headers).map(function (header) { 82 | res.setHeader(header, globalConfig.headers[header]); 83 | }); 84 | if (globalConfig.delay) sleep(globalConfig.delay); 85 | return next(); 86 | }; 87 | }; 88 | 89 | // Returns a middleware which proxies to the target 90 | // TODO: exception handling for targets which are wrong, 91 | // currently the proxy fails and breaks the server. 92 | var assignNewProxy = function assignNewProxy(target) { 93 | return proxy(target, { 94 | forwardPath: function forwardPath(req, res) { 95 | return Url.parse(req.originalUrl).path; 96 | } 97 | }); 98 | }; 99 | 100 | // Filesystems dont allow '/' in the names of folders / files, 101 | // Converting this character to a '!' 102 | var encodeRoutePath = function encodeRoutePath(route) { 103 | return route.replace(/\//g, "!"); 104 | }; 105 | var decodeRoutePath = function decodeRoutePath(route) { 106 | return route.replace(/!/g, "/"); 107 | }; 108 | 109 | // Creators and assigners - subtle abstraction : These return middlewares 110 | var createProxyRoute = function createProxyRoute(route, target) { 111 | return router.use(route, assignNewProxy(target)); 112 | }; 113 | 114 | var createStubRoute = function createStubRoute(route, stub) { 115 | return router.use(route, stubHandler(route, stub)); 116 | }; 117 | 118 | var createDynamicStubRoute = function createDynamicStubRoute(route, dynamicStub) { 119 | return router.use(route, dynamicStubHandler(route, dynamicStub)); 120 | }; 121 | 122 | var dynamicStubHandler = function dynamicStubHandler(_route, _name) { 123 | var dynamicObj = void 0; 124 | configLoop: for (var i = 0; i < config.routes.length; i++) { 125 | if (config.routes[i].route == _route) { 126 | for (var j = 0; j < config.routes[i].dynamicStubs.length; j++) { 127 | if (config.routes[i].dynamicStubs[j].name == _name) { 128 | dynamicObj = config.routes[i].dynamicStubs[j]; 129 | break configLoop; 130 | } 131 | }; 132 | }; 133 | } 134 | return dynamicStubRequestHandler(_route, dynamicObj); 135 | }; 136 | 137 | // Middleware which returns a stub 138 | var stubHandler = function stubHandler(route, stub) { 139 | return function (req, res, next) { 140 | return res.sendFile(path.join(__dirname, 'stubs', encodeRoutePath(route), stub)); 141 | }; 142 | }; 143 | 144 | // Middleware which handles the dynamic stubs conditions 145 | // Uses sandcastle to execute evals on a node sanbox. 146 | // The req object parameters are injected into the execution 147 | // runtime, so the eval expressions can access the req object. 148 | var dynamicStubRequestHandler = function dynamicStubRequestHandler(_route, _stub) { 149 | return function (req, res) { 150 | var returnedStub = _stub.defaultStub, 151 | continueLoop = true, 152 | count = 0; 153 | 154 | if (_stub.conditions.length) { 155 | async.whilst(function () { 156 | return count < _stub.conditions.length && continueLoop; 157 | }, function (callback) { 158 | var script = sandcastle.createScript('exports.main = function() {\n try {\n if (' + _stub.conditions[count].eval + ')\n exit(\'' + _stub.conditions[count].stub + '\')\n else\n exit(false)\n } catch(e) {\n exit(false)\n }\n }'); 159 | 160 | count++; 161 | 162 | script.on('exit', function (err, output) { 163 | if (output) { 164 | continueLoop = false; 165 | returnedStub = output; 166 | } 167 | callback(); 168 | }); 169 | 170 | script.run({ 171 | req: assign({}, { 172 | baseURL: req.baseURL, 173 | body: req.body, 174 | cookies: req.cookies, 175 | headers: req.headers, 176 | hostname: req.hostname, 177 | ip: req.ip, 178 | ips: req.ips, 179 | method: req.method, 180 | originalUrl: req.originalUrl, 181 | params: req.params, 182 | path: req.path, 183 | protocol: req.protocol, 184 | query: req.query, 185 | route: req.route, 186 | signedCookies: req.signedCookies, 187 | stale: req.stale, 188 | subdomains: req.subdomains, 189 | xhr: req.xhr 190 | }) 191 | }); 192 | }, function (err) { 193 | res.sendFile(path.join(__dirname, 'stubs', encodeRoutePath(_route), returnedStub)); 194 | }); 195 | } else { 196 | res.sendFile(path.join(__dirname, 'stubs', encodeRoutePath(_route), returnedStub)); 197 | } 198 | }; 199 | }; 200 | 201 | // Create and update a route 202 | // TODO: RE-Implement this and make it pretty 203 | var updateRoute = function updateRoute(_req) { 204 | var matchCount = 0; 205 | if (_req.old_route != _req.route) { 206 | if (_req.old_route) deleteroute(_req.old_route); 207 | delete _req.old_route; 208 | } else { 209 | delete _req.old_route; 210 | var _iteratorNormalCompletion = true; 211 | var _didIteratorError = false; 212 | var _iteratorError = undefined; 213 | 214 | try { 215 | for (var _iterator = router.stack[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 216 | var layer = _step.value; 217 | 218 | var match = _req.route.match(layer.regexp); 219 | if (match && match[0] == _req.route) { 220 | layer.handle = _req.handle == "stub" ? stubHandler(_req.route, _req.stub) : _req.handle == "proxy" ? assignNewProxy(_req.proxy) : dynamicStubHandler(_req.route, _req.dynamicStub); 221 | for (var i = 0; i < config.routes.length; i++) { 222 | if (config.routes[i].route == _req.route) { 223 | config.routes[i] = assign({}, config.routes[i], _req); 224 | } 225 | } 226 | matchCount++; 227 | } 228 | } 229 | } catch (err) { 230 | _didIteratorError = true; 231 | _iteratorError = err; 232 | } finally { 233 | try { 234 | if (!_iteratorNormalCompletion && _iterator.return) { 235 | _iterator.return(); 236 | } 237 | } finally { 238 | if (_didIteratorError) { 239 | throw _iteratorError; 240 | } 241 | } 242 | } 243 | } 244 | if (matchCount == 0) { 245 | if (_req.handle == "stub") { 246 | createStubRoute(_req.route, _req.stub); 247 | } else if (_req.handle == "proxy") { 248 | createProxyRoute(_req.route, _req.proxy); 249 | } else if (_req.handle == "dynamicStub") { 250 | createDynamicStubRoute(_req.route, _req.dynamicStub); 251 | } 252 | config.routes.push(assign({}, _req, { stubs: [], dynamicStubs: [] })); 253 | var dir = path.join(__dirname, 'stubs', encodeRoutePath(_req.route)); 254 | if (!fs.existsSync(dir)) fs.mkdirSync(dir); 255 | } 256 | fs.writeFile(configFile, JSON.stringify(config, null, 2)); 257 | }; 258 | 259 | // Create and update for stubs 260 | var updateStubs = function updateStubs(_req) { 261 | var matchCount = 0; 262 | routeLoop: for (var i = 0; i < config.routes.length; i++) { 263 | if (config.routes[i].route == _req.route) { 264 | for (var j = 0; j < config.routes[i].stubs.length; j++) { 265 | if (config.routes[i].stubs[j].name == _req.oldname || config.routes[i].stubs[j].name == _req.name) { 266 | config.routes[i].stubs[j].name = _req.name; 267 | config.routes[i].stubs[j].description = _req.description; 268 | fs.writeFileSync(path.join(__dirname, 'stubs', encodeRoutePath(_req.route), _req.name), _req.content); 269 | if (_req.oldname) { 270 | fs.rename(path.join(__dirname, 'stubs', encodeRoutePath(_req.route), _req.oldname), path.join(__dirname, 'stubs', encodeRoutePath(_req.route), _req.name)); 271 | } 272 | matchCount++; 273 | break routeLoop; 274 | } 275 | } 276 | if (matchCount == 0) { 277 | fs.writeFile(path.join(__dirname, 'stubs', encodeRoutePath(_req.route), _req.name), _req.content); 278 | config.routes[i].stubs.push({ name: _req.name, description: _req.description }); 279 | } 280 | } 281 | } 282 | fs.writeFile(configFile, JSON.stringify(config, null, 2)); 283 | }; 284 | 285 | // Create and update for dynamic stubs, this is very similar 286 | // to that of stubs but there is no file associated, only the 287 | // config is updated. 288 | var updateDynamicStubs = function updateDynamicStubs(_req) { 289 | var req = assign({}, _req); 290 | var matchCount = 0, 291 | oldname = req.oldname, 292 | route = req.route; 293 | delete req.oldname; 294 | delete req.route; 295 | routeLoop: for (var i = 0; i < config.routes.length; i++) { 296 | if (config.routes[i].route == route) { 297 | for (var j = 0; j < config.routes[i].dynamicStubs.length; j++) { 298 | if (config.routes[i].dynamicStubs[j].name == oldname || config.routes[i].dynamicStubs[j].name == req.name) { 299 | config.routes[i].dynamicStubs[j] = req; 300 | matchCount++; 301 | break routeLoop; 302 | } 303 | } 304 | if (matchCount == 0) { 305 | config.routes[i].dynamicStubs.push(req); 306 | } 307 | } 308 | } 309 | fs.writeFile(configFile, JSON.stringify(config, null, 2)); 310 | }; 311 | 312 | // Delete a route: 313 | // 1. Remove it from the router stack 314 | // 2. Remote it from config 315 | // 3. Delete the stubs/route folder 316 | var deleteroute = function deleteroute(_route) { 317 | var index = router.stack.map(function (layer) { 318 | return _route.match(layer.regexp); 319 | }).reduce(function (index, item, i) { 320 | return !!item ? i : index; 321 | }, 0); 322 | if (index > -1) { 323 | router.stack.splice(index, 1); 324 | rimraf(path.join(__dirname, 'stubs', encodeRoutePath(_route)), function () {}); 325 | } 326 | var newRoutes = config.routes.filter(function (route) { 327 | return route.route != _route; 328 | }); 329 | config = assign(config, { routes: newRoutes }); 330 | fs.writeFile(configFile, JSON.stringify(config, null, 2)); 331 | }; 332 | 333 | // Delete a stub: 334 | // 1. Delete the file 335 | // 2. Remove the entry from config file 336 | var deletestub = function deletestub(_route, _stub) { 337 | fs.unlinkSync(path.join(__dirname, 'stubs', encodeRoutePath(_route), _stub)); 338 | 339 | routeLoop: for (var i = 0; i < config.routes.length; i++) { 340 | if (config.routes[i].route == _route) { 341 | for (var j = 0; j < config.routes[i].stubs.length; j++) { 342 | if (config.routes[i].stubs[j].name == _stub) { 343 | config.routes[i].stubs.splice(j, 1); 344 | break routeLoop; 345 | } 346 | } 347 | } 348 | } 349 | fs.writeFile(configFile, JSON.stringify(config, null, 2)); 350 | }; 351 | 352 | // Delete a dynamic stub: 353 | // 1. Remove the entry from config file 354 | var deleteDynamicstub = function deleteDynamicstub(_route, _stub) { 355 | 356 | routeLoop: for (var i = 0; i < config.routes.length; i++) { 357 | if (config.routes[i].route == _route) { 358 | for (var j = 0; j < config.routes[i].dynamicStubs.length; j++) { 359 | if (config.routes[i].dynamicStubs[j].name == _stub) { 360 | config.routes[i].dynamicStubs.splice(j, 1); 361 | break routeLoop; 362 | } 363 | } 364 | } 365 | } 366 | fs.writeFile(configFile, JSON.stringify(config, null, 2)); 367 | }; 368 | 369 | // Expects that the dynamic stub properties are updated 370 | // checks if the route is using this dynamic stub and updates its layer.handle 371 | var updateDynamicRoutes = function updateDynamicRoutes(_route, _dynamicStub) { 372 | for (var i = 0; i < config.routes.length; i++) { 373 | if (_route == config.routes[i].route && _dynamicStub == config.routes[i].dynamicStub && "dynamicStub" == config.routes[i].handle) { 374 | var _iteratorNormalCompletion2 = true; 375 | var _didIteratorError2 = false; 376 | var _iteratorError2 = undefined; 377 | 378 | try { 379 | 380 | for (var _iterator2 = router.stack[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { 381 | var layer = _step2.value; 382 | 383 | var match = config.routes[i].route.match(layer.regexp); 384 | if (match && match[0] == config.routes[i].route) { 385 | layer.handle = dynamicStubHandler(_route, _dynamicStub); 386 | } 387 | } 388 | } catch (err) { 389 | _didIteratorError2 = true; 390 | _iteratorError2 = err; 391 | } finally { 392 | try { 393 | if (!_iteratorNormalCompletion2 && _iterator2.return) { 394 | _iterator2.return(); 395 | } 396 | } finally { 397 | if (_didIteratorError2) { 398 | throw _iteratorError2; 399 | } 400 | } 401 | } 402 | } 403 | } 404 | }; 405 | 406 | var logRequest = function logRequest(_req, _type, _method) { 407 | logger[_type][_method]({ 408 | 'route': _req.path, 409 | 'query_strings': JSON.stringify(_req.query), 410 | 'request_body': JSON.stringify(_req.body), 411 | 'ip': _req.ip 412 | }); 413 | }; 414 | 415 | // Logs all requests which do not start with '/mocknode/' 416 | router.use('/', function (req, res, next) { 417 | if (/^(?!\/mocknode\/)/.test(req.originalUrl) && /^(?!\/favicon.ico)/.test(req.originalUrl)) logRequest(req, 'accesslog', 'info'); 418 | next(); 419 | }); 420 | 421 | // Logs requests that change the configuration of mocknode 422 | router.use('/mocknode/api', function (req, res, next) { 423 | var logList = ['/modifyroute', '/deleteroute', '/modifystub', '/deletestub', '/modifydynamicstub', '/deletedynamicstub']; 424 | if (logList.indexOf(req.path) > -1) logRequest(req, 'changelog', 'info'); 425 | next(); 426 | }); 427 | 428 | router.use('/mocknode', express.static(interfaceFolder)); 429 | router.use('/mocknode/api/config', function (req, res) { 430 | return res.json(config); 431 | }); 432 | 433 | router.use('/mocknode/api/stubconfig', function (req, res) { 434 | return res.json(stubConfig); 435 | }); 436 | 437 | router.use('/mocknode/api/logs', function (req, res) { 438 | res.sendFile(path.join(__dirname, 'logs', req.query.name)); 439 | }); 440 | 441 | router.use('/mocknode/api/getstub', function (req, res) { 442 | res.sendFile(path.join(__dirname, 'stubs', encodeRoutePath(req.query.route), req.query.name)); 443 | }); 444 | 445 | router.use('/mocknode/api/modifyroute', function (req, res, next) { 446 | updateRoute(req.body); 447 | res.send({ success: true }); 448 | }); 449 | 450 | router.use('/mocknode/api/deleteroute', function (req, res, next) { 451 | deleteroute(req.query.route); 452 | res.send({ success: true }); 453 | }); 454 | 455 | router.use('/mocknode/api/modifystub', function (req, res) { 456 | updateStubs(req.body); 457 | res.send({ success: true }); 458 | }); 459 | 460 | router.use('/mocknode/api/deletestub', function (req, res) { 461 | deletestub(req.query.route, req.query.name); 462 | res.send({ success: true }); 463 | }); 464 | 465 | router.use('/mocknode/api/modifydynamicstub', function (req, res) { 466 | updateDynamicStubs(req.body); 467 | updateDynamicRoutes(req.body.route, req.body.name); 468 | res.send({ success: true }); 469 | }); 470 | 471 | router.use('/mocknode/api/deletedynamicstub', function (req, res) { 472 | deleteDynamicstub(req.query.route, req.query.name); 473 | res.send({ success: true }); 474 | }); 475 | 476 | router.use(globalHeaders(config.global)); 477 | 478 | config.routes.filter(function (configObj) { 479 | return configObj.handle == "proxy"; 480 | }).map(function (configObj) { 481 | return createProxyRoute(configObj.route, configObj.proxy); 482 | }); 483 | 484 | config.routes.filter(function (configObj) { 485 | return configObj.handle == "stub"; 486 | }).map(function (configObj) { 487 | return createStubRoute(configObj.route, configObj.stub); 488 | }); 489 | 490 | config.routes.filter(function (configObj) { 491 | return configObj.handle == "dynamicStub"; 492 | }).map(function (configObj) { 493 | return createDynamicStubRoute(configObj.route, configObj.dynamicStub); 494 | }); 495 | 496 | app.listen(port); 497 | console.log("Mocknode started on port: " + port); 498 | console.log("open 'http://localhost:" + port + "/mocknode' in your browser to configure mocknode"); 499 | 500 | // App ends 501 | })(); 502 | } 503 | 504 | -------------------------------------------------------------------------------- /dist/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.6 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{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-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning: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-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.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:focus,.btn-default:hover{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.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;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:focus,.btn-success:hover{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.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{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:focus,.btn-info:hover{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.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{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:focus,.btn-warning:hover{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.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{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:focus,.btn-danger:hover{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.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.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:focus,.dropdown-menu>li>a:hover{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:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', 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,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', 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;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', 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-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.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,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', 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:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.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,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', 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)} 6 | /*# sourceMappingURL=bootstrap-theme.min.css.map */ -------------------------------------------------------------------------------- /dist/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | --------------------------------------------------------------------------------