├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── bin ├── index.js ├── install.sh └── uninstall.sh ├── package.json ├── src ├── cli │ ├── commands │ │ ├── add.js │ │ ├── help.js │ │ ├── install.js │ │ ├── kill.js │ │ ├── list.js │ │ ├── migrate.js │ │ ├── open.js │ │ ├── rm.js │ │ ├── start.js │ │ ├── status.js │ │ ├── stop.js │ │ ├── tail.js │ │ ├── uninstall.js │ │ └── version.js │ ├── doc │ │ └── help.txt │ ├── index.js │ ├── templates │ │ ├── katon.firewall.plist │ │ ├── katon.plist │ │ └── resolver │ └── utils │ │ ├── launchctl.js │ │ ├── list-hosts.js │ │ ├── osx-minor-version.js │ │ ├── path-to-host.js │ │ └── render.js ├── config.js └── daemon │ ├── certs │ ├── server.crt │ └── server.key │ ├── control.js │ ├── dns-server.js │ ├── http-router.js │ ├── https-proxy.js │ ├── index.js │ ├── procs.js │ ├── templates │ ├── 200.html │ ├── 404.html │ ├── 502.html │ └── layout.html │ └── utils │ ├── http-router.js │ ├── render.js │ ├── tail.js │ └── timer.js └── test ├── cli └── index.js ├── daemon ├── fixtures │ ├── node-slow │ │ └── index.js │ ├── node │ │ └── index.js │ ├── python │ │ └── index.html │ ├── subdomain.node │ │ └── index.js │ └── websocket │ │ ├── client.js │ │ └── index.js ├── helper.js └── index.js └── setup.js /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | node_modules 3 | tmp 4 | .DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "0.10" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 typicode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # katon [![](https://badge.fury.io/js/katon.svg)](http://badge.fury.io/js/katon) [![](https://travis-ci.org/typicode/katon.svg?branch=master)](https://travis-ci.org/typicode/katon) 2 | 3 | --- 4 | 5 | __Note__ except if you need a particular feature in katon, please use [hotel](https://github.com/typicode/hotel). Hotel is cross-platform and doesn't require admin privileges to be installed. 6 | 7 | A huge thank you to all the [people](https://github.com/typicode/katon/graphs/contributors) who contributed to katon! 8 | 9 | --- 10 | 11 | > Access your dev servers by their names 12 | 13 | katon is a development tool that makes dev servers __accessible__ on __beautiful__ local .ka domains. It also __autostarts/stops__ them for you. 14 | 15 | ![](http://i.imgur.com/AyFpCHj.png) 16 | 17 | katon supports any server: __Node, Ruby, Python, Go, Java, PHP, ...__ that can be started with a command-line and runs on __OS X__. 18 | 19 | ### Linux, Windows 20 | 21 | Please use [hotel](https://github.com/typicode/hotel). 22 | 23 | ## Install 24 | 25 | Make sure [Node](http://nodejs.org/download/) is installed first, then: 26 | 27 | ```bash 28 | $ npm install -g katon 29 | ``` 30 | 31 | To manually install katon, you can run `sudo katon install && katon start`. 32 | 33 | _Known issue: if Apache is running, it needs to be stopped to avoid conflict with katon._ 34 | 35 | ## Add servers 36 | 37 | ```bash 38 | $ katon add 'nodemon' 39 | $ katon add 'npm start' 40 | $ katon add 'grunt server' 41 | $ katon add 'rails server --port $PORT' 42 | $ katon add 'python -m SimpleHTTPServer $PORT' 43 | $ katon add 'php -S 127.0.0.1:$PORT' 44 | ``` 45 | 46 | To add a server with a different name than its directory. 47 | 48 | ```bash 49 | $ katon add 'grunt server' my-custom-name 50 | Application is now available at http://my-custom-name.ka 51 | ``` 52 | 53 | __Note__: it's important to use `'` and not `"` to avoid `$PORT` to be evaluated. 54 | 55 | Port is dynamically set by katon using `PORT` environment variable but can be passed as a parameter using `$PORT`. 56 | 57 | In case your server doesn't accept a port parameter, you can retrieve the `PORT` environment variable in your code. For example, for a Node server you would write something like: 58 | 59 | ```javascript 60 | var port = process.env.PORT || 3000; 61 | ``` 62 | 63 | The same technique can be applied with other languages too. 64 | 65 | ## How it works 66 | - When you add a server using the `katon add` command, its configuration is saved locally to `~/.katon/hosts/` and an equivalent `~/.katon/logs/` directory is also created. 67 | - The server is not started until you make your first request to your `.ka` domain. 68 | - If no request is made to your `.ka` server within an hour, then katon automatically stops it. Therefore, Katon automatically manages resources by starting only needed servers and stopping them when they're not used. 69 | 70 | ## Subdomains 71 | 72 | When adding a server, you can access it by its URL `http://app.ka`. But you can also use subdomains (e.g. `http://foo.app.ka`, `http://bar.app.ka`, ...). 73 | 74 | If you want to map a server to a subdomain, let's say `api.app.ka`, simply use `katon add api.app`. 75 | 76 | ## Access from other devices 77 | 78 | Using [xip.io](http://xip.io/) you can access your servers from other devices (iPad, iPhone, ...) on your LAN. 79 | 80 | ``` 81 | # Let's say your local address is 192.168.1.12 82 | http://.192.168.1.12.xip.io/ 83 | ``` 84 | 85 | _You can find your local address using `ifconfig` or going to index.ka_ 86 | 87 | ## Remote access behind NAT/firewall 88 | 89 | Using [ngrok.com](http://ngrok.com/) you can share access to your servers with others, when running behind a firewall or NAT. 90 | 91 | First, follow the instructions to install ngrok, then [register on the site](https://dashboard.ngrok.com/user/signup) to enable custom subdomains. 92 | 93 | Then run ngrok with your application name as the subdomain: 94 | 95 | ``` 96 | ngrok http -subdomain app_name 80 97 | ``` 98 | 99 | This exposes port 80 to the internet on *app_name.ngrok.io*. **Use at your own risk: all of your web hosts are accessible on this port while ngrok is running.** 100 | 101 | ## Access using HTTPS 102 | 103 | You can also use HTTPS to access your servers `https://.ka`. 104 | 105 | ## Logs 106 | 107 | Server logs are stored in `~/.katon/logs/.log`, to view them you can use: 108 | 109 | ```bash 110 | $ katon tail [app_name] 111 | $ katon tail all # View all logs 112 | ``` 113 | 114 | ## Version managers 115 | 116 | katon works with any version manager, simply set the desired version before adding your server and katon will remember it. 117 | 118 | ```bash 119 | $ nvm use 0.11 && katon add 'npm start' 120 | $ rbenv local 2.0.0-p481 && katon add 'rails server --port $PORT' 121 | ``` 122 | 123 | Depending on your version manager, you may need to add environment variables. 124 | 125 | ```bash 126 | $ rvm use ruby-2.0.0-p576 && katon add 'bundle exec unicorn' --env GEM_PATH 127 | # Will use GEM_PATH previously set by rvm 128 | ``` 129 | 130 | For Node users, to keep access to katon CLI accross Node versions, add an alias to your .profile and reopen the Terminal. 131 | 132 | ```bash 133 | echo "alias katon=`which katon`" >> ~/.profile 134 | ``` 135 | 136 | ## Troubleshoot 137 | 138 | Run `katon status` or check `~/.katon/daemon.log`. 139 | 140 | If you're stuck, feel free to create an issue. 141 | 142 | ## Uninstall 143 | 144 | ```bash 145 | $ npm rm -g katon 146 | ``` 147 | 148 | This will run the uninstall script wich does basically `katon stop && sudo katon uninstall`. To remove katon completely, run also `rm -rf ~/.katon`. 149 | 150 | # Credits 151 | 152 | * [Pow](http://pow.cx/) for daemon inspiration. 153 | * [Powder](https://github.com/rodreegez/powder) for CLI inspiration. 154 | 155 | # License 156 | 157 | katon is released under the MIT License. 158 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var isRoot = require('is-root') 3 | var updateNotifier = require('update-notifier') 4 | var pkg = require('../package.json') 5 | var cli = require('../src/cli') 6 | 7 | if (!isRoot()) { 8 | updateNotifier({packageName: pkg.name, packageVersion: pkg.version}).notify() 9 | } 10 | 11 | cli.run(process.argv.slice(2)) -------------------------------------------------------------------------------- /bin/install.sh: -------------------------------------------------------------------------------- 1 | echo 'Requiring super-user privileges to modify system files' 2 | sudo node ./bin/ install 3 | node ./bin/ start 4 | 5 | -------------------------------------------------------------------------------- /bin/uninstall.sh: -------------------------------------------------------------------------------- 1 | node ./bin/ stop 2 | echo 'Requiring super-user privileges to remove system files' 3 | sudo node ./bin/ uninstall -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "katon", 3 | "version": "0.10.7", 4 | "description": "Automatically starts your development servers so that you can be more productive. Servers are accessible on local .ka domains.", 5 | "keywords": [ 6 | "node", 7 | "pow", 8 | "nodemon", 9 | "development", 10 | "dev", 11 | ".ka", 12 | "local", 13 | "automatic", 14 | "server", 15 | "start", 16 | "restart", 17 | "reload" 18 | ], 19 | "author": "Typicode ", 20 | "repository": { 21 | "type": "git", 22 | "url": "git://github.com/typicode/katon.git" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/typicode/katon/issues" 26 | }, 27 | "bin": "./bin/index.js", 28 | "preferGlobal": true, 29 | "os": [ 30 | "darwin" 31 | ], 32 | "scripts": { 33 | "test": "mocha --reporter list test/cli/index test/daemon/index", 34 | "postinstall": "sh ./bin/install.sh", 35 | "uninstall": "sh ./bin/uninstall.sh" 36 | }, 37 | "dependencies": { 38 | "chalk": "^0.5.0", 39 | "ejs": "^1.0.0", 40 | "got": "^0.3.0", 41 | "http-proxy": "^1.1.4", 42 | "is-root": "^0.1.0", 43 | "minimist": "^1.1.0", 44 | "mkdirp": "^0.5.0", 45 | "native-dns": "^0.7.0", 46 | "network-address": "^1.0.0", 47 | "opn": "^0.1.2", 48 | "respawn": "^0.4.2", 49 | "rimraf": "^2.2.8", 50 | "shell-quote": "^1.4.2", 51 | "shelljs": "^0.3.0", 52 | "tildify": "^0.2.0", 53 | "touch": "0.0.3", 54 | "update-notifier": "^0.2.0", 55 | "xtend": "^3.0.0" 56 | }, 57 | "devDependencies": { 58 | "mocha": "^1.21.4", 59 | "request": "^2.45.0", 60 | "supertest": "^0.13.0", 61 | "ws": "^0.7.0" 62 | }, 63 | "license": "MIT" 64 | } 65 | -------------------------------------------------------------------------------- /src/cli/commands/add.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var minimist = require('minimist') 3 | var mkdirp = require('mkdirp') 4 | var chalk = require('chalk') 5 | var pathToHost = require('../utils/path-to-host') 6 | var config = require('../../config') 7 | 8 | // add [name] -e [ENV] -e [ENV] 9 | module.exports = function(args) { 10 | // Make sure hosts dir exists 11 | mkdirp.sync(config.hostsDir) 12 | 13 | // Parse args 14 | args = minimist(args, { 15 | string: ['env'], 16 | alias: { e: 'env' } 17 | }) 18 | 19 | // Get command 20 | var command = args._[0] ? args._[0].trim() : '' 21 | 22 | if (command === '') { 23 | return console.log( 24 | 'Please specify a command\n' 25 | + 'katon add ' 26 | ) 27 | } 28 | 29 | // Get host name 30 | var host = args._[1] ? args._[1] : pathToHost(process.cwd()) 31 | 32 | // Create env 33 | var env = { 34 | PATH: process.env.PATH 35 | } 36 | 37 | if (typeof args.env === 'string') args.env = [args.env] 38 | 39 | for (var i in args.env) { 40 | var e = args.env[i] 41 | 42 | if (e === 'PATH') continue 43 | 44 | if (process.env.hasOwnProperty(e)) { 45 | env[e] = process.env[e] 46 | } else { 47 | return console.log('Can\'t find ' + e + ' environment variable') 48 | } 49 | } 50 | 51 | // Create host file 52 | fs.writeFileSync(config.hostsDir + '/' + host + '.json', JSON.stringify( 53 | { 54 | command : command, 55 | cwd : process.cwd(), 56 | env : env 57 | } 58 | , null, 2) 59 | ) 60 | 61 | console.log( 62 | "Application is now availaible at %s", 63 | chalk.cyan('http://' + host + '.ka/') 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /src/cli/commands/help.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | 3 | module.exports = function() { 4 | console.log(fs.readFileSync(__dirname + '/../doc/help.txt', 'utf-8')) 5 | } 6 | -------------------------------------------------------------------------------- /src/cli/commands/install.js: -------------------------------------------------------------------------------- 1 | var isRoot = require('is-root') 2 | var chalk = require('chalk') 3 | var sh = require('shelljs') 4 | var render = require('../utils/render') 5 | var version = require('../utils/osx-minor-version') 6 | var config = require('../../config.js') 7 | 8 | sh.config.silent = true 9 | 10 | module.exports = function() { 11 | if (!isRoot()) { 12 | return console.log(chalk.red('katon install requires root privileges')) 13 | } 14 | 15 | console.log(chalk.green('Installing katon')) 16 | 17 | // Create domain resolver 18 | render('resolver', config.resolverPath) 19 | 20 | // Create and load firewall plist 21 | render('katon.firewall.plist', config.firewallPlistPath, { mode: 33188 }) 22 | 23 | if (version() >= 10) { 24 | sh.exec('launchctl bootstrap system ' + config.firewallPlistPath) 25 | sh.exec('launchctl enable system/katon.firewall') 26 | sh.exec('launchctl kickstart -k system/katon.firewall') 27 | } else { 28 | sh.exec('launchctl load -Fw ' + config.firewallPlistPath) 29 | } 30 | 31 | // Done 32 | console.log(chalk.green('Done')) 33 | } -------------------------------------------------------------------------------- /src/cli/commands/kill.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var chalk = require('chalk') 4 | var touch = require('touch') 5 | var pathToHost = require('../utils/path-to-host') 6 | var config = require('../../config') 7 | 8 | module.exports = function(args) { 9 | var host = args[0] || pathToHost(process.cwd()) 10 | var file = config.hostsDir + '/' + host + '.json' 11 | if (fs.existsSync(file)) { 12 | touch.sync(file) 13 | console.log('%s has been successfully stopped', chalk.cyan(host)) 14 | console.log('To restart simply go to http://%s.ka', host) 15 | } else { 16 | console.log("Can\'t find %s, use katon list", chalk.red(host)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/cli/commands/list.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var chalk = require('chalk') 3 | var tildify = require('tildify') 4 | var listHosts = require('../utils/list-hosts') 5 | var config = require('../../config.js') 6 | 7 | module.exports = function() { 8 | listHosts().forEach(function(name) { 9 | var filename = config.hostsDir + '/' + name 10 | var host = JSON.parse(fs.readFileSync(filename)) 11 | 12 | console.log( 13 | chalk.cyan(name.replace('.json', '')), 14 | host.command, 15 | chalk.grey(tildify(host.cwd)) 16 | ) 17 | }) 18 | 19 | console.log() 20 | 21 | console.log( 22 | chalk.grey('Go to http://index.ka to view app list from your browser') 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/cli/commands/migrate.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var chalk = require('chalk') 4 | var add = require('./add') 5 | 6 | module.exports = function() { 7 | console.log('Migration script to katon 0.5.x') 8 | 9 | fs.readdirSync(process.env.HOME + '/.katon').forEach(function(name) { 10 | var filename = process.env.HOME + '/.katon/' + name 11 | var isSymbolicLink = fs.lstatSync(filename).isSymbolicLink() 12 | 13 | if (isSymbolicLink) { 14 | var dir = path.resolve(fs.readlinkSync(filename)) 15 | 16 | try { 17 | console.log(chalk.grey('\nMigrate path: ' + dir)) 18 | var dotKatonPath = dir + '/.katon' 19 | var command = fs.existsSync(dotKatonPath) 20 | ? (fs.readFileSync(dotKatonPath, 'utf-8').trim()) 21 | : 'npm start' 22 | 23 | console.log(chalk.grey('Add path: ' + dir + ' command: ' + command)) 24 | add(command, dir) 25 | fs.unlinkSync(dotKatonPath) 26 | } catch(e) { 27 | console.log(chalk.red('Failed to migrate ' + dir + '\n' + e)) 28 | } 29 | 30 | fs.unlinkSync(filename) 31 | } 32 | }) 33 | 34 | console.log('\nDone, run `katon list` to verify') 35 | } -------------------------------------------------------------------------------- /src/cli/commands/open.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var minimist = require('minimist') 3 | var open = require('opn') 4 | var chalk = require('chalk') 5 | var pathToHost = require('../utils/path-to-host') 6 | 7 | // open [name] --https 8 | module.exports = function(args) { 9 | var args = minimist(args) 10 | var host = args._[0] || pathToHost(process.cwd()) 11 | var protocol = args.https ? 'https' : 'http' 12 | var url = protocol + '://' + host + '.ka/' 13 | 14 | console.log('Opening', chalk.cyan(url)) 15 | open(url) 16 | } 17 | -------------------------------------------------------------------------------- /src/cli/commands/rm.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var chalk = require('chalk') 4 | var config = require('../../config') 5 | 6 | // rm [name] 7 | module.exports = function(args) { 8 | var host = args[0] || path.basename(process.cwd()) 9 | var conf = config.hostsDir + '/' + host + '.json' 10 | var logFile = config.logsDir + '/' + host + '.log' 11 | 12 | // Remove log file 13 | if (fs.existsSync(logFile)) fs.unlinkSync(logFile) 14 | 15 | // Remove server conf 16 | if (fs.existsSync(conf)) { 17 | fs.unlinkSync(conf) 18 | console.log("Sucessfully removed %s", chalk.cyan(host)) 19 | } else { 20 | console.log("Can\'t find %s, use katon list", chalk.red(host)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/cli/commands/start.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var sh = require('shelljs') 3 | var mkdirp = require('mkdirp') 4 | var chalk = require('chalk') 5 | var render = require('../utils/render') 6 | var version = require('../utils/osx-minor-version') 7 | var config = require('../../config.js') 8 | 9 | sh.config.silent = true 10 | 11 | module.exports = function() { 12 | console.log(chalk.green('Starting katon daemon')) 13 | 14 | mkdirp.sync(path.dirname(config.hostsDir)) 15 | 16 | // Create and load daemon plist 17 | render('katon.plist', config.daemonPlistPath, { mode: 33188 }) 18 | 19 | if (version() >= 10) { 20 | var UID = process.getuid() 21 | sh.exec('launchctl bootstrap gui/' + UID + ' ' + config.daemonPlistPath) 22 | sh.exec('launchctl enable gui/' + UID + '/katon') 23 | sh.exec('launchctl kickstart -k gui/' + UID + '/katon') 24 | } else { 25 | sh.exec('launchctl unload ' + config.daemonPlistPath) 26 | sh.exec('launchctl load -Fw ' + config.daemonPlistPath) 27 | } 28 | 29 | // Done 30 | console.log(chalk.green('Done')) 31 | } 32 | -------------------------------------------------------------------------------- /src/cli/commands/status.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var got = require('got') 3 | var sh = require('shelljs') 4 | var config = require('../../config.js') 5 | 6 | module.exports = function() { 7 | console.log('Checking that katon daemon is loaded...') 8 | 9 | var output = sh.exec('launchctl list | grep \'katon\'', { silent: true }).output 10 | daemonLoaded = output.indexOf('katon\n') !== -1 11 | 12 | if (daemonLoaded) { 13 | console.log(chalk.green('OK')) 14 | } else { 15 | return console.log(chalk.red('KO daemon is not loaded, try `katon start`')) 16 | } 17 | 18 | console.log('Checking .ka domain...') 19 | 20 | got('http://index.ka', function(err, data, res) { 21 | if (err) { 22 | console.log(chalk.red('KO try `sudo katon install`')) 23 | } else { 24 | console.log(chalk.green('OK')) 25 | } 26 | }) 27 | } -------------------------------------------------------------------------------- /src/cli/commands/stop.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var launchctl = require('../utils/launchctl') 3 | var config = require('../../config.js') 4 | 5 | module.exports = function() { 6 | console.log(chalk.red('Stopping katon daemon, please wait')) 7 | launchctl.remove(config.daemonPlistPath) 8 | console.log(chalk.red('Done')) 9 | } -------------------------------------------------------------------------------- /src/cli/commands/tail.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var config = require('../../config.js') 3 | var fs = require('fs') 4 | var listHosts = require('../utils/list-hosts') 5 | var Tail = require('../../daemon/utils/tail') 6 | 7 | 8 | // Number of lines to show from end of log. 9 | var INITIAL_LINES = 10 10 | 11 | // Return host name for current working directory 12 | // Directory usually, not necessarily, same as host name 13 | function getHostName(cwd) { 14 | var hostNames = listHosts() 15 | .map(function(name) { 16 | var filename = config.hostsDir + '/' + name 17 | var host = JSON.parse(fs.readFileSync(filename)) 18 | return [name.replace('.json', ''), host.cwd] 19 | }) 20 | .filter(function(nameAndPath) { 21 | var path = nameAndPath[1] 22 | return path === cwd 23 | }) 24 | .map(function(nameAndPath) { 25 | var name = nameAndPath[0] 26 | return name 27 | }) 28 | return hostNames[0] 29 | } 30 | 31 | var COLORS = ['blue', 'magenta', 'green', 'yellow', 'cyan', 'gray']; 32 | var mappingColors = {}; 33 | var idx = 0; 34 | 35 | function colorPrefix (prefix) { 36 | if (!mappingColors.hasOwnProperty(prefix)) { 37 | if (idx >= COLORS.length) { 38 | idx = 0; 39 | } 40 | mappingColors[prefix] = COLORS[idx++]; 41 | } 42 | return chalk[mappingColors[prefix]](prefix); 43 | } 44 | 45 | 46 | module.exports = function(args) { 47 | var host = args[0] || getHostName(process.cwd()) 48 | var tail = new Tail(host, INITIAL_LINES); 49 | tail 50 | .on('line', function(prefix, line) { 51 | if (prefix) 52 | process.stdout.write(colorPrefix('[' + prefix + '] ')) 53 | process.stdout.write(line + '\n') 54 | }) 55 | .on('error', function(error) { 56 | process.stderr.write(chalk.red(error.message) + '\n') 57 | process.exit(1) 58 | }) 59 | tail.start() 60 | 61 | // Keep process running until Ctrl+C 62 | setInterval(function() { 63 | }, 1000) 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/cli/commands/uninstall.js: -------------------------------------------------------------------------------- 1 | var isRoot = require('is-root') 2 | var sh = require('shelljs') 3 | var rmrf = require('rimraf') 4 | var chalk = require('chalk') 5 | var launchctl = require('../utils/launchctl') 6 | var config = require('../../config.js') 7 | 8 | module.exports = function() { 9 | if (!isRoot()) { 10 | return console.log(chalk.red('katon uninstall requires root privileges')) 11 | } 12 | 13 | console.log(chalk.red('Uninstalling katon')) 14 | 15 | // Remove domain resolver 16 | rmrf.sync(config.resolverPath) 17 | 18 | // Remove firewall 19 | var result = sh.exec('pfctl -a com.apple/250.KatonFirewall -F all', { silent: true }) 20 | if (result.code !== 0) { 21 | console.log(result.output) 22 | } 23 | 24 | launchctl.remove(config.firewallPlistPath) 25 | 26 | console.log(chalk.red('Done')) 27 | } 28 | -------------------------------------------------------------------------------- /src/cli/commands/version.js: -------------------------------------------------------------------------------- 1 | var pkg = require('../../../package.json') 2 | 3 | module.exports = function() { 4 | console.log(pkg.version) 5 | } -------------------------------------------------------------------------------- /src/cli/doc/help.txt: -------------------------------------------------------------------------------- 1 | 2 | Usage: 3 | katon add [app_name] Add server and use current dir name or app_name 4 | katon rm [app_name] Remove server by current dir name or app_name 5 | katon open [app_name] [--https] Open current dir or app_name in browser 6 | katon kill [app_name] Kill server using current dir or app_name 7 | katon list List apps 8 | katon tail [app_name] Tail server log for current dir or app_name 9 | katon tail all Tail server log for all apps 10 | katon start|stop Daemon control 11 | katon install|uninstall Must be run to setup katon (use sudo) 12 | katon status Give status informations 13 | 14 | Examples: 15 | katon add 'nodemon app.js' 16 | katon add 'grunt server watch' 17 | katon add 'python -m SimpleHTTPServer $PORT' 18 | 19 | To setup katon: 20 | sudo katon install && katon start 21 | -------------------------------------------------------------------------------- /src/cli/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | '-v' : require('./commands/version'), 4 | '--version' : require('./commands/version'), 5 | 6 | help : require('./commands/help'), 7 | 8 | add : require('./commands/add'), 9 | rm : require('./commands/rm'), 10 | kill : require('./commands/kill'), 11 | list : require('./commands/list'), 12 | open : require('./commands/open'), 13 | status : require('./commands/status'), 14 | start : require('./commands/start'), 15 | stop : require('./commands/stop'), 16 | tail : require('./commands/tail'), 17 | install : require('./commands/install'), 18 | uninstall : require('./commands/uninstall'), 19 | 20 | migrate : require('./commands/migrate'), 21 | 22 | run: function(args) { 23 | var command = args[0] 24 | var options = args.slice(1) 25 | 26 | if (this[command]) { 27 | this[command](options) 28 | } else { 29 | this.help() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/cli/templates/katon.firewall.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | katon.firewall 7 | ProgramArguments 8 | 9 | /bin/sh 10 | -c 11 | 12 | sysctl -w net.inet.ip.forwarding=1; 13 | echo "rdr pass inet proto tcp from any to self port {80,<%= httpPort %>} -> 127.0.0.1 port <%= httpPort %>\nrdr pass inet proto tcp from any to self port {443,<%= httpsProxyPort %>} -> 127.0.0.1 port <%= httpsProxyPort %>" | pfctl -a "com.apple/250.KatonFirewall" -Ef - 14 | 15 | 16 | RunAtLoad 17 | 18 | UserName 19 | root 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/cli/templates/katon.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | katon 7 | ProgramArguments 8 | 9 | <%= nodePath %> 10 | <%= daemonPath %> 11 | 12 | KeepAlive 13 | 14 | RunAtLoad 15 | 16 | StandardOutPath 17 | <%= daemonLogPath %> 18 | StandardErrorPath 19 | <%= daemonLogPath %> 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/cli/templates/resolver: -------------------------------------------------------------------------------- 1 | # Katon 2 | nameserver 127.0.0.1 3 | port <%= dnsPort %> 4 | -------------------------------------------------------------------------------- /src/cli/utils/launchctl.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var sh = require('shelljs') 3 | var rimraf = require('rimraf') 4 | var render = require('./render') 5 | 6 | module.exports = { 7 | 8 | create: function(templateName, dest) { 9 | render(templateName, dest, { mode: 33188 }) 10 | var result = sh.exec('launchctl load -Fw ' + dest) 11 | if (result.code !== 0) console.log(result.output) 12 | }, 13 | 14 | remove: function(filename) { 15 | if (fs.existsSync(filename)) { 16 | var result = sh.exec('launchctl unload ' + filename) 17 | if (result.code !== 0) console.log(result.output) 18 | rimraf.sync(filename) 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/cli/utils/list-hosts.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var mkdirp = require('mkdirp') 3 | var config = require('../../config.js') 4 | 5 | module.exports = function() { 6 | mkdirp.sync(config.hostsDir) 7 | 8 | return fs.readdirSync(config.hostsDir) 9 | } 10 | -------------------------------------------------------------------------------- /src/cli/utils/osx-minor-version.js: -------------------------------------------------------------------------------- 1 | var sh = require('shelljs') 2 | 3 | module.exports = function() { 4 | return +sh.exec('sw_vers -productVersion').output.split('.')[1] 5 | } -------------------------------------------------------------------------------- /src/cli/utils/path-to-host.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | 3 | // Convert a path to a host name 4 | // Ex: /path/My_App -> my-app 5 | module.exports = function(dir) { 6 | return path.basename(dir).replace(/_/g, '-').toLowerCase() 7 | } -------------------------------------------------------------------------------- /src/cli/utils/render.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var mkdirp = require('mkdirp') 4 | var ejs = require('ejs') 5 | var config = require('../../config') 6 | 7 | // Render templates from ../templates to dest 8 | module.exports = function(templateName, dest, options) { 9 | 10 | var template = fs.readFileSync(__dirname + '/../templates/' + templateName, 'utf-8') 11 | var data = ejs.render(template, config) 12 | 13 | mkdirp.sync(path.dirname(dest)) 14 | fs.writeFileSync(dest, data, options) 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var HOME = process.env.HOME 3 | 4 | module.exports = { 5 | nodePath : process.execPath, 6 | 7 | dnsPort : 13375, 8 | httpsProxyPort : 30900, 9 | httpPort : 31000, 10 | 11 | resolverPath : '/etc/resolver/ka', 12 | 13 | firewallPlistPath : '/Library/LaunchDaemons/katon.firewall.plist', 14 | daemonPlistPath : HOME + '/Library/LaunchAgents/katon.plist', 15 | hostsDir : HOME + '/.katon/hosts', 16 | logsDir : HOME + '/.katon/logs', 17 | daemonPath : path.resolve(__dirname + '/daemon/index'), 18 | daemonLogPath : HOME + '/.katon/daemon.log' 19 | } 20 | -------------------------------------------------------------------------------- /src/daemon/certs/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICnDCCAYQCCQCcAPSh0PyTBTANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQKDAVr 3 | YXRvbjAeFw0xNDEwMTcyMjE3MjZaFw0xNTEwMTcyMjE3MjZaMBAxDjAMBgNVBAoM 4 | BWthdG9uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz57nLErdLsDk 5 | abl8DgRYPbrzP/mNwCoO4O8Hu/POBE6+i20FXd82tA9ymfBNTpL7EATfu48stI7M 6 | jUNS8+s1p+Hj6vZ0HvmQ+gXyMP/A1mpH1aw9JFBWYgS/P+HBD25SShB5TAYUDb0a 7 | v4d0lj49LixWRqGuyNGO7K75cmNGtJyZIe+a+G9nza4l+HqNztaz2Km+Cqh2K7Nt 8 | 4xIGBw+ZXBfF74cPFYIk9ndxVjhlep3RPg9VLWAbyHGyVzdcp5hbrqP5Ryp6XpO0 9 | 2PyEBqlQ+6LKvXD79e0sDURHMn0q8mWuDEeA2fAt6H58mDLSahau4O/pFvjFt2wL 10 | a6da/4f8dQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCX33On/veC9JIvK5JxSeay 11 | oC+wLMI/vwt385O3j/D8IG4XG8thwA9DjFbt8GDXTmDAFhG6ZIe9kg3n2AbMONGu 12 | U7AevmR5ZbrouCuHmBfk11X0OAMDvXdXDYyPGV0Zh5qK3VvbhZe90Uefo1T6PhA5 13 | qydd9UxgJo8DlePgcY+keUy9gPjXwdAHLgPf245WS560rkDTmQyq8lWBwniYuTvw 14 | kOm9fnjwSWYYxxd2iWBP0T+sj5BRZSSIztkSm8F68GBt18tqnkU3s75UwZFtXBNh 15 | gxk0Bev2qy8m9ONPDEWaxVH6+xlKMk838j7tPHq9aNv/MpxilMOzKowiWPb8Pe3B 16 | -----END CERTIFICATE----- 17 | -------------------------------------------------------------------------------- /src/daemon/certs/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAz57nLErdLsDkabl8DgRYPbrzP/mNwCoO4O8Hu/POBE6+i20F 3 | Xd82tA9ymfBNTpL7EATfu48stI7MjUNS8+s1p+Hj6vZ0HvmQ+gXyMP/A1mpH1aw9 4 | JFBWYgS/P+HBD25SShB5TAYUDb0av4d0lj49LixWRqGuyNGO7K75cmNGtJyZIe+a 5 | +G9nza4l+HqNztaz2Km+Cqh2K7Nt4xIGBw+ZXBfF74cPFYIk9ndxVjhlep3RPg9V 6 | LWAbyHGyVzdcp5hbrqP5Ryp6XpO02PyEBqlQ+6LKvXD79e0sDURHMn0q8mWuDEeA 7 | 2fAt6H58mDLSahau4O/pFvjFt2wLa6da/4f8dQIDAQABAoIBADwOPdZNDW+xsiB2 8 | 29B+Jzwr8KLnv73/LHCaE8WlP0l1sZ5I+c1ufLdW5JJstR/uWhsHHeR2BLtxtu+B 9 | suQFfG7EY5YalfpDvFDmGWldAV3EPmUrPkBb0LDnqJ6E4cBh7AGqhDueYnya37rZ 10 | Jrsy46WQg6BIsnM6UrpZ3qPc3Z3uTGEWBlVRGxUoR6lMpBnWSBznXq9lVsEmSVd9 11 | ED3LEXdT+M0oF/JWDVT+Y6jD/cV8FPJEx4y4h4k/l/LbhIj2eZPT60bNSUCmrWWn 12 | RmPm7mpol6nwU+UcJSgZvjctBj2NjtS2ruhWNp4xxRfrbKyH80mgGj4dJczy4Jdg 13 | 567KjiECgYEA/BLTncKCY6J3/Ch7xzSHFhtE6uB5BDgnSHw7bie4KFlKNm10bvYH 14 | 52EYZjkwXsQuCW2JOimk0b8+tcrlWhlLAeNLmnayKxW9GDeaPS6YX15ZYLs1oNBq 15 | PTB8rTok5r5EzOeXo0AeEx2GZhx0d4fevtawT9FLyxYMG5j+K2ws91kCgYEA0trQ 16 | wFQySaUD1eAyPC2D//QlpOAZyEOPPtVPnzh+D2Imic6830NUz69wL1tRUezEJjRZ 17 | 10+f2FjAPP9wEKzBDD+0X5Z9Fue3e+DN5DhY3PY5wv6Zbco8V78gL15hL+YhcQON 18 | 2oIUIKXvagnQk+YxnsANA3rq7JWrwX/3ucg8Jn0CgYBQLvDvuwLdDL5cEMim6lea 19 | OZxnlnYIWJBuZ05EURAsjZKk05Z5AXwsJt+rDMANNRxr1VMUlFCgg9Q/4cWpLmiE 20 | tjfDb8RnHigjfvRqR9siYxHNSl/ZwtI4mqbeN6OrXpTmFTlQLcIjVH3/F0gZCbha 21 | PlKhYTNZ6654TOd1CpkXKQKBgQCWvfLBnS/6cGuGiuq/FPcqlFwZPFGSV6JgFFYB 22 | CX0t+Eh++vsSTmuistTsNkez3yX3/jNAd99Z51FACooOkcLNw/lq4QZ6ypvlhzkK 23 | 8LGu/qUa37PGxu9O+AfFdZ7bhJXh2t2eGqLTGG5KC8w/ADH3QWvMUiMDkpkhFCCB 24 | hEJkbQKBgFyeRWS/jYCA1wiYvTfhdukw8tpJ0tqeGQFyZH0Szzi9794TnA1SOMyI 25 | 8YgkMj84eWcXOUshZBq5dUog5z0IiQ57UDoly9aLHl6u4ubIKhaMQgO/y9UY9e4m 26 | TSxLs6egBaQOwbc76REEuxq6Z8zPwGX4/UxzQ+Hg5pfLHjqPNPCx 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /src/daemon/control.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var chalk = require('chalk') 3 | var util = require('util') 4 | var dnsServer = require('./dns-server') 5 | var httpsProxy = require('./https-proxy') 6 | var httpRouter = require('./http-router') 7 | var procs = require('./procs') 8 | var config = require('../config') 9 | 10 | function log(str) { 11 | util.log(chalk.red('[daemon] ') + str) 12 | } 13 | 14 | module.exports = { 15 | 16 | start: function() { 17 | log('Start') 18 | 19 | this.dns = dnsServer.createServer() 20 | this.https = httpsProxy.createServer() 21 | this.http = httpRouter.createServer() 22 | 23 | log('Loading procs') 24 | procs.load() 25 | 26 | log('Starting DNS server on port ' + config.dnsPort) 27 | this.dns.serve(config.dnsPort, function() { 28 | log('DNS server started') 29 | }) 30 | 31 | log('Starting HTTPS server on port ' + config.httpsProxyPort) 32 | this.https.listen(config.httpsProxyPort, function() { 33 | log('HTTPS server started') 34 | }) 35 | 36 | log('Starting HTTP server on port ' + config.httpPort) 37 | this.http.listen(config.httpPort, function() { 38 | log('HTTP server started') 39 | }) 40 | }, 41 | 42 | stop: function(callback) { 43 | log('Stop') 44 | 45 | this.http.close(function() { 46 | log('HTTP server stopped') 47 | }) 48 | 49 | this.https.close(function() { 50 | log('HTTPS server stopped') 51 | }) 52 | 53 | this.dns.close(function() { 54 | log('DNS server stopped') 55 | }) 56 | 57 | for (var id in procs.list) { 58 | procs.list[id].stop() 59 | } 60 | 61 | setTimeout(callback, 1000) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/daemon/dns-server.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | var dns = require('native-dns') 3 | var chalk = require('chalk') 4 | 5 | function log(str) { 6 | util.log(chalk.magenta('[dns ] ')) 7 | } 8 | 9 | module.exports.createServer = function() { 10 | 11 | var server = dns.createServer() 12 | 13 | server.on('request', function(request, response) { 14 | var question = request.question[0] 15 | var name = question.name 16 | 17 | var a = dns.A({ 18 | name: name, 19 | address: '127.0.0.1', 20 | ttl: 600 21 | }) 22 | var aaaa = dns.AAAA({ 23 | name: name, 24 | address: '::1', 25 | ttl: 600 26 | }) 27 | 28 | // Answer A question with A record, AAAA with AAAA 29 | // record (and vice versa) 30 | if (question.type === dns.consts.NAME_TO_QTYPE.A) 31 | response.answer.push(a) 32 | if (question.type === dns.consts.NAME_TO_QTYPE.AAAA) 33 | response.answer.push(aaaa) 34 | 35 | response.send() 36 | log('Resolved ' + chalk.grey(name)) 37 | }) 38 | 39 | server.on('error', function (err, buff, req, res) { 40 | log(chalk.grey(err.stack)) 41 | }) 42 | 43 | return server 44 | } 45 | -------------------------------------------------------------------------------- /src/daemon/http-router.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var httpProxy = require('http-proxy') 3 | var net = require('net') 4 | var address = require('network-address') 5 | var chalk = require('chalk') 6 | var procs = require('./procs') 7 | var render = require('./utils/render') 8 | var Tail = require('./utils/tail') 9 | var timer = require('./utils/timer') 10 | var util = require('util') 11 | 12 | // Logger 13 | function log(id, msg, err) { 14 | var str = chalk.green('[router]') + ' ' + msg 15 | str += err ? ' [' + err + '] ' : ' ' 16 | str += chalk.grey(id) 17 | util.log(str) 18 | } 19 | 20 | // For http://www.app.ka will return ['www', 'app'] 21 | // For http://www.app.10.0.0.1.xip.io will return ['www', 'app'] 22 | // For http://www.app.ngrok.io will return ['www', 'app'] 23 | function removeTopLevelDomain(host) { 24 | if (/.xip.io$/.test(host)) { 25 | return host.split('.').slice(0, -6) 26 | } else if (/.ngrok.io$/.test(host)) { 27 | return host.split('.').slice(0, -2) 28 | } else { 29 | return host.split('.').slice(0, -1) 30 | } 31 | } 32 | 33 | // For http://www.app.ka will return app 34 | function getDomainId(host) { 35 | return removeTopLevelDomain(host).pop() 36 | } 37 | 38 | // For http://www.app.ka will return www.app 39 | function getSubdomainId(host) { 40 | return removeTopLevelDomain(host).join('.') 41 | } 42 | 43 | // Find procId based on host name 44 | function getProcId(host) { 45 | var domainId = getDomainId(host) 46 | var subdomainId = getSubdomainId(host) 47 | 48 | return procs.list[subdomainId] ? subdomainId : domainId 49 | } 50 | 51 | // Find proc based on host name 52 | function getProc(host) { 53 | return procs.list[getProcId(host)] 54 | } 55 | 56 | // Return true if proc exist 57 | function procExists(host) { 58 | return typeof getProc(host) != 'undefined' 59 | } 60 | 61 | // Find, start, return proc based on host name 62 | // Proc is automatically stopped after 1 hour if this function is not called 63 | function startProc(host) { 64 | var proc = getProc(host) 65 | 66 | try { 67 | proc.start() 68 | } catch (e) { 69 | util.log(host, 'Can\'t start proc', e) 70 | } 71 | 72 | timer(proc.id, function() { 73 | log(proc.id, 'No requests for 1 hour') 74 | proc.stop() 75 | }) 76 | 77 | return proc 78 | } 79 | 80 | function tailSSE(res) { 81 | res.writeHead(200, { 82 | 'Content-Type': 'text/event-stream; charset=utf-8', 83 | 'Cache-Control': 'no-cache', 84 | 'Connection': 'keep-alive' 85 | }); 86 | var tail = new Tail('all', 10) 87 | tail 88 | .on('line', function(prefix, line) { 89 | var message = { 90 | prefix: prefix, 91 | line: line.replace(/\[\d{2}m/g, '') 92 | } 93 | res.write('event: line\ndata: ' + JSON.stringify(message) + '\n\n'); 94 | }) 95 | .on('error', function(error) { 96 | log('all', 'Cannot tail logs', error) 97 | }) 98 | res.on('close', function() { 99 | tail.stop() 100 | }) 101 | tail.start() 102 | } 103 | 104 | 105 | module.exports.createServer = function() { 106 | 107 | var server = http.createServer() 108 | var proxy = httpProxy.createProxyServer() 109 | 110 | // WebSocket 111 | server.on('upgrade', function(req, socket, head) { 112 | var host = req.headers.host 113 | var count = 0 114 | var max = 9 115 | 116 | log(host, 'WebSocket request received') 117 | 118 | // Test if proc exists for host 119 | if (!procExists(host)) { 120 | return log(host, 'Can\'t find proc') 121 | } 122 | 123 | // Start process or refresh it's timer 124 | var proc = startProc(host) 125 | 126 | // Forward 127 | function forward() { 128 | // proxy.ws can be only used once, so we're trying to make a TCP connect 129 | // before to know if port is open 130 | var client = net.connect({port: proc.env.PORT}, function() { 131 | // WebSocket request can be proxied, destroy TCP socket 132 | client.destroy() 133 | log(host, 'Proxying to ws://127.0.0.1:' + proc.env.PORT) 134 | proxy.ws(req, socket, head, 135 | { target: 'ws://127.0.0.1:' + proc.env.PORT }, 136 | function(err) { 137 | log(host, 'Can\'t proxy WebSocket request') 138 | } 139 | ) 140 | }) 141 | 142 | client.on('error', function(err) { 143 | log(host, 'Can\'t connect to ws://127.0.0.1:' + proc.env.PORT, err) 144 | count += 1 145 | if (count <= max) { 146 | log(host, 'retry in 1 second') 147 | setTimeout(function() { 148 | forward() 149 | }, 1000) 150 | } 151 | }) 152 | } 153 | 154 | forward() 155 | }) 156 | 157 | // HTTP 158 | server.on('request', function(req, res) { 159 | var host = req.headers.host 160 | var count = 0 161 | var max = 3 162 | 163 | log(host, 'HTTP request received') 164 | 165 | // Render katon home 166 | var domain = getDomainId(host) 167 | if (domain === 'index' || domain === 'katon') { 168 | if (req.url == '/tail') { 169 | tailSSE(res) 170 | return 171 | } else { 172 | return res.end(render('200.html', { 173 | procs: procs.list, 174 | ip: address() 175 | })) 176 | } 177 | } 178 | 179 | // Verify host is set and valid 180 | if (!(/.ka$/.test(host) || /.xip.io$/.test(host) || /.ngrok.io$/.test(host))) { 181 | log(host, 'Not a valid Host') 182 | res.statusCode = 200 183 | return res.end(render('200.html', { 184 | procs: procs.list, 185 | ip: address() 186 | })) 187 | } 188 | 189 | // Test if proc exists for host 190 | if (!procExists(host)) { 191 | log(host, 'Can\'t find proc') 192 | res.statusCode = 404 193 | return res.end(render('404.html')) 194 | } 195 | 196 | // Start process or refresh it's timer 197 | var proc = startProc(host) 198 | 199 | // Proxy request 200 | function forward() { 201 | log(host, 'Proxying to http://127.0.0.1:' + proc.env.PORT) 202 | proxy.web(req, res, { 203 | target: 'http://127.0.0.1:' + proc.env.PORT 204 | }, function(err, req, res) { 205 | // If address is in use stop 206 | if (err.code === 'EADDRINUSE') { 207 | log(host, err.code + ' check that port is not in use') 208 | res.statusCode = 502 209 | return res.end(render('502.html', { 210 | name: getProcId(host) 211 | })) 212 | } 213 | 214 | // Else retry 215 | log(host, 'Can\'t connect, retry in 1 second [' + err + ']') 216 | count += 1 217 | if (count <= max) { 218 | setTimeout(function() { 219 | forward() 220 | }, 1000) 221 | } else { 222 | log(host, 'Can\'t connect: ' + err) 223 | res.statusCode = 502 224 | res.end(render('502.html', { 225 | name: getProcId(host) 226 | })) 227 | } 228 | }) 229 | } 230 | 231 | forward() 232 | }) 233 | 234 | return server 235 | } 236 | -------------------------------------------------------------------------------- /src/daemon/https-proxy.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var httpProxy = require('http-proxy') 3 | var config = require('../config') 4 | 5 | module.exports.createServer = function() { 6 | return httpProxy.createServer({ 7 | target: { 8 | host: 'localhost', 9 | port: config.httpPort 10 | }, 11 | ssl: { 12 | key: fs.readFileSync(__dirname + '/certs/server.key', 'utf8'), 13 | cert: fs.readFileSync(__dirname + '/certs/server.crt', 'utf8') 14 | } 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/daemon/index.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | var chalk = require('chalk') 3 | var control = require('./control') 4 | 5 | chalk.enabled = true 6 | 7 | control.start() 8 | 9 | process.on('SIGTERM', function() { 10 | util.log('Received SIGTERM') 11 | util.log('Stop') 12 | }) -------------------------------------------------------------------------------- /src/daemon/procs.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var util = require('util') 4 | var respawn = require('respawn') 5 | var extend = require('xtend') 6 | var mkdirp = require('mkdirp') 7 | var chalk = require('chalk') 8 | var tildify = require('tildify') 9 | var parse = require('shell-quote').parse 10 | var config = require('../../src/config') 11 | 12 | var PORT = config.httpPort 13 | 14 | function createLogger(id) { 15 | return function(str) { 16 | util.log(chalk.cyan('[procs ] ') + str + ' ' + chalk.gray(id)) 17 | } 18 | } 19 | 20 | module.exports = { 21 | list: {}, 22 | 23 | add: function(id, options) { 24 | var log = createLogger(id) 25 | var env = extend(process.env, options.env) 26 | 27 | log('Add') 28 | 29 | // Set PORT 30 | env.PORT = PORT += 2 31 | 32 | // Create proc and add it to the procs list 33 | var proc = this.list[id] = respawn({ 34 | command : parse(options.command, { PORT: env.PORT }), 35 | cwd : options.cwd, 36 | env : env, 37 | maxRestarts : -1, 38 | sleep : 10*1000 39 | }) 40 | 41 | // Ensure logs directory exist 42 | mkdirp.sync(config.logsDir) 43 | 44 | // Create log stream 45 | var logFile = config.logsDir + '/' + id + '.log' 46 | 47 | var out = fs.createWriteStream(logFile) 48 | .on('error', log) 49 | .on('open', function() { 50 | proc.stdio = ['ignore', out, out] 51 | }) 52 | 53 | // Listen to proc events 54 | proc.on('start', function() { 55 | log('Start') 56 | out.write( 57 | '[katon] Starting ' + id + ' on port: '+ proc.env.PORT 58 | + ' using command: ' + proc.command.join(' ') 59 | + '\n' 60 | ) 61 | }) 62 | .on('exit', function() { 63 | log('Stop') 64 | // Empty log file on exit 65 | fs.writeFile(logFile, '', function() {}) 66 | }) 67 | .on('warn', log) 68 | 69 | }, 70 | 71 | remove: function(id) { 72 | var log = createLogger(id) 73 | log('Remove') 74 | this.list[id].stop() 75 | delete this.list[id] 76 | }, 77 | 78 | 79 | load: function() { 80 | var self = this 81 | 82 | // Ensure hosts dir exists 83 | mkdirp.sync(config.hostsDir) 84 | 85 | // Read and add config files 86 | fs.readdirSync(config.hostsDir).forEach(function(name) { 87 | if (/.json$/.test(name)) { 88 | var id = name.replace('.json', '') 89 | var options = JSON.parse(fs.readFileSync(config.hostsDir + '/' + name)) 90 | 91 | self.add(id, options) 92 | } 93 | }) 94 | 95 | // Watch for changes 96 | fs.watch(config.hostsDir, function(event, name) { 97 | var id = name.replace('.json', '') 98 | var file = config.hostsDir + '/' + name 99 | 100 | if (fs.existsSync(file)) { 101 | var options = JSON.parse(fs.readFileSync(file)) 102 | if (self.list[id]) self.remove(id) 103 | self.add(id, options) 104 | } else { 105 | self.remove(id) 106 | } 107 | }) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/daemon/templates/200.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <% Object.keys(procs).forEach(function(name) { %> 9 | 10 | 13 | 16 | 19 | 22 | 23 | <% }) %> 24 |
localxip.io *commandstatus
11 | <%= name %>.ka/ 12 | 14 | http://<%= name %>.<%= ip %>.xip.io 15 | 17 | <%= procs[name].command.join(' ') %> 18 | 20 | <%= procs[name].status %> 21 |
25 | 26 |

* xip.io is a wildcard DNS service, you can use these addresses to access your development web server from devices on your local network, like iPads, iPhones, and other computers.

27 |

28 | 
29 | 
47 | 


--------------------------------------------------------------------------------
/src/daemon/templates/404.html:
--------------------------------------------------------------------------------
1 | 

Can't find server, check that it has been added to katon using katon list

-------------------------------------------------------------------------------- /src/daemon/templates/502.html: -------------------------------------------------------------------------------- 1 |

Can't connect, possible reasons:

2 | 3 |
    4 |
  • Server can't be started.
  • 5 |
  • Server is not listening on the right port.
  • 6 |
  • Server is taking more than 4 seconds to start.
  • 7 |
8 | 9 |

Try to reload or run katon tail <%= name %>

10 | -------------------------------------------------------------------------------- /src/daemon/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Katon 5 | 6 | 11 | 12 | 13 | 18 |
19 | <%- yield %> 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /src/daemon/utils/http-router.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var httpProxy = require('http-proxy') 3 | 4 | module.exports = { 5 | createServer: function() { 6 | 7 | var server = http.createServer() 8 | var proxy = httpProxy.createProxyServer() 9 | 10 | server.forward = function(req, res, port, delay) { 11 | var target = { target : 'http://localhost:' + port } 12 | 13 | setTimeout(function() { 14 | proxy.web(req, res, target, function(err) { 15 | if (err) { 16 | setTimeout(function() { 17 | proxy.web(req, res, target, function(err) { 18 | server.emit('error', err, req, res) 19 | }) 20 | }, delay) 21 | } 22 | }) 23 | }, delay) 24 | } 25 | 26 | return server 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/daemon/utils/render.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var util = require('util') 3 | var ejs = require('ejs') 4 | 5 | module.exports = function(templateName, context) { 6 | 7 | context = context || {} 8 | 9 | var templateDir = __dirname + '/../templates' 10 | var templatePath = templateDir + '/' + templateName 11 | var layoutPath = templateDir + '/layout.html' 12 | 13 | try { 14 | var template = fs.readFileSync(templatePath, 'utf-8') 15 | var layout = fs.readFileSync(layoutPath, 'utf-8') 16 | 17 | return ejs.render(layout, { 18 | yield: ejs.render(template, context) 19 | }) 20 | } catch(e) { 21 | util.log('Unexpected error: ' + e) 22 | return e.toString() 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/daemon/utils/tail.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var Events = require('events') 3 | var fs = require('fs') 4 | var path = require('path') 5 | var config = require('../../config.js') 6 | var Util = require('util') 7 | 8 | 9 | function log(msg, err) { 10 | var str = chalk.blue('[tail]') + ' ' + msg 11 | str += err ? ' [' + err + '] ' : ' ' 12 | Util.log(str) 13 | } 14 | 15 | 16 | function Tail(host, initialLines) { 17 | this.host = host || 'all' 18 | this.initialLines = initialLines 19 | this.logFiles = null 20 | } 21 | Util.inherits(Tail, Events.EventEmitter) 22 | 23 | 24 | // Call this to start tailing 25 | Tail.prototype.start = function() { 26 | if (this.logFiles) 27 | return 28 | 29 | if (this.host == 'all') { 30 | this.logFiles = this.tailAllLogFiles() 31 | } else { 32 | var filename = config.logsDir + '/' + this.host + '.log' 33 | var logFile = new LogFile(this, filename, this.host) 34 | logFile.start() 35 | this.logFiles = [ logFile ] 36 | } 37 | } 38 | 39 | 40 | // Call this to end tailing 41 | Tail.prototype.stop = function() { 42 | // log('Stopped tailing') 43 | this.logFiles.forEach(function(logFile) { 44 | logFile.stop() 45 | }) 46 | } 47 | 48 | 49 | // -- Tail log files -- 50 | 51 | // Tail all log files from the logs directory 52 | Tail.prototype.tailAllLogFiles = function() { 53 | var tail = this 54 | var logFiles = fs 55 | .readdirSync(config.logsDir) 56 | .filter(function(filename) { 57 | return /\.log$/.test(filename) 58 | }) 59 | .map(function(filename) { 60 | return config.logsDir + '/' + filename 61 | }) 62 | .map(function(filename) { 63 | var host = path.basename(filename, '.log') 64 | var logFile = new LogFile(tail, filename, host) 65 | logFile.start() 66 | return logFile 67 | }) 68 | return logFiles 69 | } 70 | 71 | 72 | // Maintains state for each log file 73 | function LogFile(tail, filename, host) { 74 | this.filename = filename 75 | this.host = host 76 | this.position = 0 77 | this.initialLines = tail.initialLines 78 | this.emit = tail.emit.bind(tail) 79 | } 80 | 81 | 82 | LogFile.prototype.start = function() { 83 | var logFile = this 84 | //log('Tailing ' + logFile.filename) 85 | 86 | var stream = fs.createReadStream(logFile.filename, { encoding: 'utf8' }) 87 | var lines = [] 88 | var buffer = '' 89 | stream.on('data', function(chunk) { 90 | buffer += chunk 91 | // Parse stream into lines and keep the last INITIAL_LINES in memory. 92 | // Make sure position points to the last byte in the file, or the first 93 | // byte of the last incomplete line. 94 | var match 95 | while (match = buffer.match(/^.*\n/)) { 96 | var line = match[0] 97 | lines.push(line.slice(0, -1)) 98 | logFile.position += line.length 99 | buffer = buffer.slice(line.length) 100 | } 101 | lines = lines.slice(-logFile.initialLines) 102 | }) 103 | stream.on('end', function() { 104 | // Dump the tail of the log to the console and start watching for changes. 105 | //for (var i = 0; i < lines.length ; ++i) 106 | for (var i in lines) 107 | logFile.emit('line', logFile.host, lines[i]) 108 | logFile.watch() 109 | }) 110 | stream.on('error', function(error) { 111 | logFile.emit('error', new Error('Cannot tail ' + logFile.filename + ', start server and try again')) 112 | }) 113 | } 114 | 115 | 116 | // Watch file for changes. 117 | LogFile.prototype.watch = function() { 118 | var logFile = this 119 | var streaming = false 120 | 121 | this.watcher = fs.watch(this.filename, { persistent: false }, changeEvent) 122 | 123 | function changeEvent() { 124 | if (streaming) 125 | return 126 | 127 | var stats = fs.statSync(logFile.filename) 128 | if (stats.size < logFile.position) { 129 | // File got truncated need to adjust position to new end of file 130 | logFile.position = stats.size 131 | } else if (stats.size > logFile.position) { 132 | streaming = true 133 | logFile.tailStream(function() { 134 | streaming = false 135 | }) 136 | } 137 | } 138 | } 139 | 140 | 141 | // Tail the end of the stream from the previous position. 142 | LogFile.prototype.tailStream = function(callback) { 143 | var logFile = this 144 | var streamOptions = { 145 | start: logFile.position, 146 | encoding: 'utf8' 147 | } 148 | var stream = fs.createReadStream(logFile.filename, streamOptions) 149 | var lastLine = '' 150 | stream.on('data', function(chunk) { 151 | var buffer = lastLine + chunk 152 | var lines = buffer.split(/\n/) 153 | lastLine = lines.pop() 154 | for (var i in lines) { 155 | var line = lines[i] 156 | logFile.emit('line', logFile.host, line) 157 | logFile.position += line.length + 1 158 | } 159 | }) 160 | stream.on('end', callback); 161 | stream.on('error', function(error) { 162 | logFile.emit('error', new Error('Cannot tail ' + logFile.filename + ', start server and try again')) 163 | }) 164 | } 165 | 166 | 167 | LogFile.prototype.stop = function() { 168 | this.watcher.close() 169 | } 170 | 171 | 172 | module.exports = Tail 173 | -------------------------------------------------------------------------------- /src/daemon/utils/timer.js: -------------------------------------------------------------------------------- 1 | var timeouts = {} 2 | 3 | module.exports = function(id, fn) { 4 | clearTimeout(timeouts[id]) 5 | timeouts[id] = setTimeout(fn, 60 * 60 * 1000) 6 | } 7 | -------------------------------------------------------------------------------- /test/cli/index.js: -------------------------------------------------------------------------------- 1 | var setup = require('../setup')() 2 | 3 | var assert = require('assert') 4 | var fs = require('fs') 5 | var cli = require('../../src/cli') 6 | var config = require('../../src/config') 7 | var sh = require('shelljs') 8 | 9 | sh.exec = function() { return { code: 0, output: '' } } 10 | 11 | // Mostly testing that it doesn't crash 12 | // Integration testing is done in /test/daemon 13 | describe('katon command-line interface', function() { 14 | it('install', function() { 15 | // root 16 | process.getuid = function() { 17 | return 0 18 | } 19 | 20 | cli.run(['install']) 21 | assert(fs.existsSync(config.resolverPath)) 22 | assert(fs.existsSync(config.firewallPlistPath)) 23 | }) 24 | 25 | it('uninstall', function() { 26 | // root 27 | process.getuid = function() { 28 | return 0 29 | } 30 | 31 | cli.run(['uninstall']) 32 | assert(!fs.existsSync(config.resolverPath)) 33 | assert(!fs.existsSync(config.firewallPlistPath)) 34 | }) 35 | 36 | it('start', function() { 37 | process.getuid = function() { 38 | return 1000 39 | } 40 | 41 | cli.run(['start']) 42 | assert(fs.existsSync(config.daemonPlistPath)) 43 | }) 44 | 45 | it('stop', function() { 46 | process.getuid = function() { 47 | return 1000 48 | } 49 | 50 | cli.run(['stop']) 51 | assert(!fs.existsSync(config.daemonPlistPath)) 52 | }) 53 | 54 | it('status', function() { 55 | cli.run(['status']) 56 | }) 57 | 58 | it('list/add/list/rm', function() { 59 | cli.run(['list']) 60 | cli.run(['add', 'echo']) 61 | cli.run(['add', 'echo', 'foo']) 62 | cli.run(['add', 'echo', 'foo.bar']) 63 | cli.run(['add', 'echo', 'foo.bar', '-e', 'FOO']) 64 | cli.run(['add', 'echo', 'foo.bar', '-e', 'PATH']) 65 | cli.run(['add', 'echo', 'foo.bar', '-e', 'HOME']) 66 | cli.run(['list']) 67 | cli.run(['rm']) 68 | cli.run(['rm', 'foo']) 69 | cli.run(['rm', 'foo.bar']) 70 | cli.run(['list']) 71 | }) 72 | 73 | it('touch', function() { 74 | cli.run(['add', 'echo']) 75 | cli.run(['add', 'echo', 'foo']) 76 | cli.run(['kill']) 77 | cli.run(['kill', 'foo']) 78 | cli.run(['kill', 'bar']) 79 | }) 80 | 81 | it('version', function() { 82 | cli.run(['--version']) 83 | cli.run(['-v']) 84 | }) 85 | 86 | it('help', function() { 87 | cli.run(['help']) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /test/daemon/fixtures/node-slow/index.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | 3 | setTimeout(function() { 4 | 5 | http.createServer(function (req, res) { 6 | console.log('Received a request') 7 | res.writeHead(200, {'Content-Type': 'text/plain'}) 8 | res.end('OK'); 9 | }).listen(process.env.PORT, function() { 10 | console.log('listening on port', process.env.PORT) 11 | }) 12 | 13 | }, 5000) 14 | -------------------------------------------------------------------------------- /test/daemon/fixtures/node/index.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | 3 | http.createServer(function (req, res) { 4 | console.log('Received a request') 5 | res.writeHead(200, {'Content-Type': 'text/plain'}) 6 | res.end('OK'); 7 | }).listen(process.env.PORT, function() { 8 | console.log('listening on port', process.env.PORT) 9 | }) 10 | -------------------------------------------------------------------------------- /test/daemon/fixtures/python/index.html: -------------------------------------------------------------------------------- 1 | OK -------------------------------------------------------------------------------- /test/daemon/fixtures/subdomain.node/index.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | 3 | http.createServer(function (req, res) { 4 | console.log('Received a request') 5 | res.writeHead(200, {'Content-Type': 'text/plain'}) 6 | res.end('SUB'); 7 | }).listen(process.env.PORT, function() { 8 | console.log('listening on port', process.env.PORT) 9 | }) 10 | -------------------------------------------------------------------------------- /test/daemon/fixtures/websocket/client.js: -------------------------------------------------------------------------------- 1 | // Use it to test ./index.js 2 | var WebSocket = require('ws') 3 | var ws = new WebSocket('ws://127.0.0.1:' + process.env.PORT, { 4 | host: 'foo' 5 | }) 6 | 7 | ws.on('open', function() { 8 | console.log('Open') 9 | }) 10 | -------------------------------------------------------------------------------- /test/daemon/fixtures/websocket/index.js: -------------------------------------------------------------------------------- 1 | var WebSocketServer = require('ws').Server 2 | var wss = new WebSocketServer({ port: process.env.PORT }) 3 | 4 | wss.on('connection', function() { 5 | console.log('Connection') 6 | }) 7 | -------------------------------------------------------------------------------- /test/daemon/helper.js: -------------------------------------------------------------------------------- 1 | var rmrf = require('rimraf') 2 | var chalk = require('chalk') 3 | var config = require('../../src/config') 4 | var request = require('supertest')('http://localhost:' + config.httpPort) 5 | var cli = require('../../src/cli') 6 | 7 | function log(str) { 8 | console.log() 9 | console.log(chalk.green(str)) 10 | console.log() 11 | } 12 | 13 | module.exports = { 14 | 15 | GET: function(host, options, done) { 16 | setTimeout(function() { 17 | options.status = options.status ? options.status : 200 18 | 19 | log('GET http://' + host + ' expect ' + options.status) 20 | 21 | request 22 | .get('/') 23 | .set('Host', host) 24 | .expect(options.status ? options.status : 200) 25 | .end(done) 26 | }, 1000) 27 | }, 28 | 29 | add: function(host, command) { 30 | log('CLI add host:' + host + ' command:' + command) 31 | process.cwd = function() { 32 | return __dirname + '/fixtures/' + host 33 | } 34 | command ? cli.add([command]) : cli.add([]) 35 | }, 36 | 37 | remove: function(host) { 38 | log('CLI remove host:' + host) 39 | cli.rm([host]) 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /test/daemon/index.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | var WebSocket = require('ws') 3 | var setup = require('../setup')() 4 | var helper = require('./helper') 5 | var config = require('../../src/config') 6 | 7 | describe('Katon', function() { 8 | this.timeout(10000); 9 | 10 | before(function(done) { 11 | // This will start daemon 12 | require('../../src/daemon') 13 | setTimeout(done, 1000) 14 | }) 15 | 16 | after(function(done) { 17 | require('../../src/daemon/control').stop() 18 | setTimeout(done, 1000) 19 | }) 20 | 21 | it('Doesn\'t add node > GET http://node.ka', function(done) { 22 | helper.add('node') // no command provided 23 | 24 | helper.GET('node.ka', { 25 | status: 404 26 | }, done) 27 | }) 28 | 29 | it('Add node > GET http://node.10.0.0.1.xip.io', function(done) { 30 | helper.add('node', 'node index.js') 31 | 32 | helper.GET('node.10.0.0.1.xip.io', { 33 | status: 200, 34 | body: 'OK' 35 | }, done) 36 | }) 37 | 38 | it('Add node > GET http://node.ka', function(done) { 39 | helper.add('node', 'node index.js') 40 | 41 | helper.GET('node.ka', { 42 | status: 200, 43 | body: 'OK' 44 | }, done) 45 | }) 46 | 47 | it('Add node > GET http://foo.node.ka', function(done) { 48 | helper.add('node', 'node index.js') 49 | 50 | helper.GET('foo.node.ka', { 51 | status: 200, 52 | body: 'OK' 53 | }, done) 54 | }) 55 | 56 | it('Add node as subdomain > GET http://subdomain.node.ka', function(done) { 57 | helper.add('subdomain.node', 'node index.js') 58 | 59 | helper.GET('subdomain.node.ka', { 60 | status: 200, 61 | body: 'SUB' 62 | }, done) 63 | }) 64 | 65 | it('Remove node > GET http://node.ka', function(done) { 66 | helper.remove('node') 67 | 68 | helper.GET('node.ka', { 69 | status: 404 70 | }, done) 71 | }) 72 | 73 | it('Remove node subdomain > GET http://subdomain.node.ka', function(done) { 74 | helper.remove('subdomain.node') 75 | 76 | helper.GET('subdomain.node.ka', { 77 | status: 404 78 | }, done) 79 | }) 80 | 81 | it('Add node-slow > GET http://node-slow.ka', function(done) { 82 | helper.add('node-slow', 'node index.js') 83 | 84 | helper.GET('node-slow.ka', { 85 | status: 502 86 | }, done) 87 | }) 88 | 89 | it('GET http://node-slow.ka (reload)', function(done) { 90 | setTimeout(function() { 91 | helper.GET('node-slow.ka', { 92 | status: 200 93 | }, done) 94 | }, 5500) 95 | }) 96 | 97 | it('add python > GET http://python.ka', function(done) { 98 | helper.add('python', 'python -m SimpleHTTPServer $PORT') 99 | 100 | helper.GET('python.ka', { 101 | status: 200 102 | }, done) 103 | }) 104 | 105 | it('remove python > GET http://python.ka', function(done) { 106 | helper.remove('python') 107 | 108 | helper.GET('python.ka', { 109 | status: 404 110 | }, done) 111 | }) 112 | 113 | it('Add non existing path', function(done) { 114 | helper.add('foo', 'node') 115 | 116 | helper.GET('foo.ka', { 117 | status: 502 118 | }, done) 119 | }) 120 | 121 | it('GET http://localhost', function(done) { 122 | helper.GET('localhost', { 123 | status: 200 124 | }, done) 125 | }) 126 | 127 | it('GET http://127.0.0.1', function(done) { 128 | helper.GET('127.0.0.1', { 129 | status: 200 130 | }, done) 131 | }) 132 | 133 | it('GET https://127.0.0.1 (HTTPS test)', function(done) { 134 | request({ 135 | url: 'https://127.0.0.1:' + config.httpsProxyPort, 136 | strictSSL: false, 137 | rejectUnhauthorized: false 138 | }, function(err, response, body) { 139 | if (!err && response.statusCode === 200) { 140 | done() 141 | } else { 142 | done(err) 143 | } 144 | }) 145 | }) 146 | 147 | it('Supports WebSocket', function(done) { 148 | helper.add('websocket', 'node index.js') 149 | 150 | setTimeout(function() { 151 | var ws = new WebSocket('ws://127.0.0.1:' + config.httpPort, { 152 | host: 'websocket.ka', 153 | origin: 'websocket.ka' 154 | }) 155 | 156 | ws.on('open', function() { 157 | done() 158 | }) 159 | 160 | ws.on('error', function(err) { 161 | done(new Error(err)) 162 | }) 163 | }, 1000) 164 | }) 165 | }) 166 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var fs = require('fs') 3 | var rmrf = require('rimraf') 4 | var mkdirp = require('mkdirp') 5 | 6 | // Some configs depends on HOME, it must be changed before requiring config 7 | var tmp = process.env.HOME = __dirname + '/../tmp' 8 | var config = require('../src/config') 9 | 10 | config.resolverPath = tmp + config.resolverPath 11 | config.firewallPlistPath = tmp + config.firewallPlist 12 | 13 | config.dnsPort = 50100 14 | config.httpPort = 50200 15 | 16 | // Setup paths so that everything is written to tmp 17 | module.exports = function() { 18 | rmrf.sync(tmp) 19 | mkdirp.sync(tmp + '/etc/resolver') 20 | mkdirp.sync(tmp + '/Library/LaunchDaemons') 21 | mkdirp.sync(tmp + '/Library/LaunchAgents') 22 | } --------------------------------------------------------------------------------