├── .gitignore ├── start.js ├── example.vee ├── keys ├── vee.crt └── vee.key ├── package.json ├── LICENSE ├── README.md ├── app.coffee └── proxy.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('coffee-script/register') 3 | require('./app.coffee') 4 | -------------------------------------------------------------------------------- /example.vee: -------------------------------------------------------------------------------- 1 | name: "my-breakfast-app" 2 | routes: 3 | ".*/static(\\-[0-9]+\\.[0-9]+)?/": "http://localhost:3333" 4 | "/my-breakfast-app/[0-9]+/$": "http://localhost:3333/my-breakfast-app/static/html/index.html" 5 | ".*": "http://localhost:8081/" -------------------------------------------------------------------------------- /keys/vee.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBlDCB/gIJAMCAJ4LMoKJbMA0GCSqGSIb3DQEBBQUAMA4xDDAKBgNVBAoTA1Zl 3 | ZTAgFw0xMzA5MTAxNjExNTVaGA8yMjg3MDYyNTE2MTE1NVowDjEMMAoGA1UEChMD 4 | VmVlMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDXHbG89AmfEQNwJ+9Ieh1H 5 | cfV2pqcplU1hbeDe9fX+5r5Sbc5jn6me/qnoLU9vOsyCD3LuxRBNKagRbh9U+7mn 6 | 7Ss2yKk166l5UtL4rngMB6lCHu8e/3/if0X5IRWSid59JPbTjRAwNPsuWnJqaUP6 7 | YQBZzTXYss6wzXFxnO4RmwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAC5wfi2KOBXV 8 | twG2X9TGgt8CyOUh6HRS5X9ef3a/17QFjtgXHVdKsyeE9Lped40mCvT7AQ9MLV5c 9 | JhKf7xjxYd3Hqip0qdmvegWKY22rWYqHNUozUQLV58ARl8nzQ/G3NFhafhOosYWx 10 | e37NelruOMpMrYbUgGq2jTUa2E4R5wrM 11 | -----END CERTIFICATE----- 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vee", 3 | "version": "1.2.0", 4 | "description": "Proxy to make local development of static apps easy", 5 | "main": "start.js", 6 | "preferGlobal": true, 7 | "bin": "./start.js", 8 | "engines": { 9 | "node": ">=0.10" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git@github.com:HubSpot/vee.git" 14 | }, 15 | "author": "Zack Bloom ", 16 | "dependencies": { 17 | "coffee-script": "~1.7.1", 18 | "colors": "~0.6.0-1", 19 | "commander": "~1.1.1", 20 | "underscore": "~1.4.4", 21 | "request": "~2.25.0", 22 | "js-yaml": "~2.1.0", 23 | "send": "~0.2.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /keys/vee.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQDXHbG89AmfEQNwJ+9Ieh1HcfV2pqcplU1hbeDe9fX+5r5Sbc5j 3 | n6me/qnoLU9vOsyCD3LuxRBNKagRbh9U+7mn7Ss2yKk166l5UtL4rngMB6lCHu8e 4 | /3/if0X5IRWSid59JPbTjRAwNPsuWnJqaUP6YQBZzTXYss6wzXFxnO4RmwIDAQAB 5 | AoGAEnx6ycIvEXxkzz6fUZss6PbiIVAxDWHHRJgsuvxoJ3+s22iUU/tkmEVKVKmQ 6 | Kf7jlEm/lsVYoUjzKIxOJP989Mj0inNPCSR2QCuniZSUVt66/5axkeyjBheSG75b 7 | BP+XZ8IsvJSH6m2hlGm9fCCPEX7wWk5IKIJi8A+3kUnywAECQQD+yMqRWMnW0H1F 8 | PupXYUnaQwljvtUM62YQnIzmxdvtfEBj22NyknpaAgU0/TDVXhGtUntqGOTTHm4k 9 | 0kYfatRrAkEA2CRzIcrD7CXda8X/+KNUODk1BJcre89FZ75c7xhJw9HnOlt+l2Cq 10 | sbM/542vfB3Qyt4s/qMwfEJUuEZlmmuDkQJBAJRcM6ijPRT7Xpa95hNvsWOI+aS9 11 | cK4PPOWbY2jV+hS174C2NZh3twtGdeW/MXptRnnYt8i2KjjNVXHXh8GvoXUCQDjD 12 | IcU/FcLfks2p2vY2FnkJdQX2mUHThhufn5Je44hciIgH3S/uAAXY2DB8QhirT9jn 13 | DhP1UVdgqBUYz7gFAjECQE3sY+8+BgkMTUey/e/6E+I/5XTzK+Jh6+TXkDCpw+gV 14 | 2fSypIm5UlZ/G5uDCkNTfLeHVvRR4aLjrud0KSN9CR8= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 HubSpot, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## :rotating_light: Project status :rotating_light: 2 | 3 | **This project is no longer maintained by HubSpot.** 4 | 5 | If you're a developer and you're looking for an alternative to Vee, check out [node-http-proxy](https://github.com/nodejitsu/node-http-proxy). 6 | 7 | If you're with an organization would like to take ownership of Vee, reach out to us! We'd be happy to see this project in the hands of a team that can give it the love it deserves. 8 | 9 | vee 10 | === 11 | 12 | Vee is a simple proxy to to develop static js apps locally. It allows you to forward traffic to various services or folders on your machine or the internet based on the url requested. 13 | 14 | It's different than other options, because the proxy configuration is read from the project (like a package.json), not a central file on your machine. 15 | 16 | Your project needs to have a `.vee` yaml configuration file (see [example.vee](https://github.com/HubSpot/vee/blob/master/example.vee)). Run 17 | `vee` in that directory and your proxying will begin. 18 | 19 | Add the `--debug` option to see each route as it matches. 20 | 21 | Getting Started 22 | ------------ 23 | 24 | #### 1. Install vee 25 | 26 | ```bash 27 | npm install -g vee 28 | ``` 29 | 30 | #### 2. Save a `.vee` file in the root of your project, with whatever routing you might need: 31 | 32 | ```yaml 33 | name: "my-app" 34 | routes: 35 | ".*/static/": "http://localhost:3333" 36 | ".*": "http://localhost:8081/" 37 | ``` 38 | 39 | #### 3. Run vee to start proxing in that directory 40 | 41 | ```bash 42 | sudo vee 43 | ``` 44 | 45 | .vee files 46 | ---------- 47 | 48 | Your .vee file should define a mapping between a regular expression to match the url 49 | requested and a host to send the request to. 50 | 51 | If the host ends with a slash ('/'), the passed in path will be appended to it, if it 52 | does not, the request will be forward to the exact page provided. Note that YAML has 53 | it's own escaping, so if you need to use the escape character ('\') in your regular 54 | expressions, use it twice ('\\\\'). 55 | 56 | See above for an example .vee file. 57 | 58 | Static files 59 | ------------ 60 | 61 | vee can also serve static files for you. Just start the target in your .vee file with 62 | the `file://` protocol. 63 | 64 | HTTPS 65 | ----- 66 | 67 | vee will by default attach to port 80 for HTTP traffic and port 443 for HTTPS traffic. 68 | vee includes some self-signed certs which should be just good enough for you to be 69 | able to use HTTPS locally (but should never be trusted to secure anything). 70 | 71 | If you would like to disable https, pass `-s 0`, or set `httpsPort: 0` in your config 72 | file. 73 | 74 | System Configuration 75 | -------------------- 76 | 77 | You can define a `~/.vee.yaml` file to set defaults for vee's command line flags 78 | and routes. For example, your vee.yaml file could contain: 79 | 80 | ```yaml 81 | default: 82 | debug: true 83 | port: 7 84 | routes: 85 | "google/.*": "http://google.com/" 86 | contacts-ui: 87 | port: 8888 88 | ``` 89 | 90 | Multiple Configurations 91 | ----------------------- 92 | 93 | You may want to have multiple configuration files within the same project, in order to allow different proxying rules depending on the envirnoment you are working on (e.g. local vs QA). You can specify a custom config file by using the `--config` flag as follows: 94 | 95 | ```bash 96 | vee --config .vee.qa 97 | ``` 98 | -------------------------------------------------------------------------------- /app.coffee: -------------------------------------------------------------------------------- 1 | _ = require('underscore') 2 | yaml = require('js-yaml') 3 | colors = require('colors') 4 | fs = require('fs') 5 | commander = require('commander') 6 | 7 | proxy = require('./proxy') 8 | 9 | NumberList = (str) -> 10 | str.split(',').map(Number) 11 | 12 | DEFAULTS = 13 | port: 80 14 | httpsPort: 443 15 | passRedirects: true 16 | 17 | commander 18 | .usage("Run it in the root of your project to start proxying requests as defined in the project's .vee file") 19 | .option('-p, --port [80]', "Port to serve http requests from. Comma seperate to bind onto multiple ports.", NumberList) 20 | .option('-s, --https-port [443]', "Port to serve https requests from (0 to disable).", NumberList) 21 | .option('-d, --debug', "Output route matching debug info.", Boolean) 22 | .option('-r, --pass-redirects', "Pass 3XXs to the browser, rather than following them.", Boolean) 23 | .option('--ssl-key ', "SSL private key file to be used with HTTPS requests") 24 | .option('--ssl-cert ', "SSL certificate file to be used with HTTPS requests") 25 | .option('-c, --config ', 'Specify a configuration file. Defaults to ./.vee', '.vee') 26 | .parse(process.argv) 27 | 28 | watcher = null 29 | watch = (file) -> 30 | watcher?.close() 31 | fs.watch file, {persistent: false}, -> 32 | waitForFileToExist file, -> 33 | console.log "A config file changed, restarting".yellow 34 | restart() 35 | 36 | waitForFileToExist = (file, callback) -> 37 | start = +(new Date) 38 | waitTime = 100 39 | do checkFile = -> 40 | fs.exists file, (exists) -> 41 | if exists 42 | callback() 43 | else if +(new Date) - start < waitTime 44 | setImmediate checkFile 45 | else 46 | console.error "configuration file not found within #{waitTime}ms".red 47 | process.exit(1) 48 | 49 | loadCfg = (file) -> 50 | cfg = fs.readFileSync(file).toString('utf8') 51 | 52 | watch file 53 | 54 | try 55 | return yaml.safeLoad(cfg) 56 | catch e 57 | console.error "Config file at #{ file } is not valid YAML: #{ e.toString() }".red 58 | process.exit(1) 59 | 60 | start = -> 61 | # Options can come from four sources: 62 | # 63 | # - The project's .vee file 64 | # - Project specific options in ~/.vee.yaml (in a section titled the project's .name property) 65 | # - Defaults in the system's ~/.vee.yaml (in the `default` section) 66 | # - Command line flags 67 | 68 | try 69 | project = loadCfg commander.config 70 | catch e 71 | if e.code is 'ENOENT' 72 | console.error "configuration file not found in the current directory".red 73 | process.exit(1) 74 | else 75 | throw e 76 | 77 | try 78 | system = loadCfg "#{ process.env.HOME }/.vee.yaml" 79 | catch e 80 | throw e unless e.code is 'ENOENT' 81 | 82 | defaults = system?['default'] ? {} 83 | 84 | personal = {} 85 | if project.name? and system?[project.name]? 86 | personal = system[project.name] 87 | 88 | options = _.extend {}, DEFAULTS, defaults, project, personal, _.pick(commander, 'port', 'httpsPort', 'debug', 'passRedirects', 'sslKey', 'sslCert') 89 | 90 | options.httpPort = options.httpPort ? options.port 91 | delete options.port 92 | 93 | unless _.isArray options.httpPort 94 | options.httpPort = [options.httpPort] 95 | 96 | if options.httpsPort and not _.isArray options.httpsPort 97 | options.httpsPort = [options.httpsPort] 98 | 99 | #this will ensure that it goes through the project routes FIRST, before personal, then defaults (stepped through by 100 | #insertion order). `_.defaults` was used so personal don't overwrite project routes and default routes don't overwrite 101 | #project or personal routes. 102 | options.routes = _.extend {}, project.routes 103 | _.defaults options.routes, personal.routes 104 | _.defaults options.routes, defaults.routes 105 | 106 | proxy.start options 107 | 108 | stop = -> 109 | proxy.stop() 110 | 111 | restart = -> 112 | stop() 113 | start() 114 | 115 | start() 116 | -------------------------------------------------------------------------------- /proxy.coffee: -------------------------------------------------------------------------------- 1 | colors = require('colors') 2 | http = require('http') 3 | https = require('https') 4 | request = require('request') 5 | domain = require('domain') 6 | fs = require('fs') 7 | send = require('send') 8 | URL = require('url') 9 | _ = require('underscore') 10 | 11 | httpsServer = server = null 12 | 13 | start = (config) -> 14 | match = (url, route) -> 15 | new RegExp(route).exec(url) 16 | 17 | debug = (args...) -> 18 | if config.debug 19 | console.log args... 20 | 21 | getTarget = (req) -> 22 | debug "Proxying request to #{ req.url }" 23 | for path, dest of config.routes 24 | debug "Trying #{ path }" 25 | 26 | if matches = match("#{ req.headers.host }/#{ req.url }", path) 27 | for component, i in matches when i > 0 28 | dest = dest.replace new RegExp("\\$#{ i }", 'g'), component 29 | 30 | debug "#{ path } matches, sending to #{ dest }".green 31 | 32 | return dest 33 | 34 | debug "No match!".red 35 | 36 | handle = (req, res) -> 37 | reqDomain = domain.create() 38 | 39 | target = getTarget(req) 40 | 41 | reqDomain.on 'error', (err) -> 42 | console.log 'Error Proxying!'.red 43 | console.log 'Request:', req?.url 44 | console.log 'Target:', target 45 | console.log err?.code or err 46 | 47 | res.writeHead 502 48 | res.end "vee error proxying: #{ err?.code or err }" 49 | 50 | unless target 51 | res.writeHead 404 52 | res.end "Proxying target not found" 53 | return 54 | 55 | url = target 56 | 57 | if target[target.length - 1] is '/' 58 | url += URL.parse(req.url).path.replace(/^\//, '') 59 | 60 | if URL.parse(target).protocol is 'file:' 61 | url = url.substr(7) 62 | 63 | reqDomain.run -> 64 | send(req, url).pipe res 65 | 66 | else 67 | # Ensure that the Host header does not conflict with the request URL, except 68 | # when making requests to the local machine. 69 | if req.headers?.host and not url.match(/^(local\.)|(localhost:)/) 70 | delete req.headers.host 71 | 72 | options = 73 | uri: url 74 | method: req.method 75 | headers: req.headers or {} 76 | followRedirect: not config.passRedirects 77 | 78 | reqDomain.run -> 79 | unless config.disableFastEtagCheck 80 | # If the request has an Etag that matches the URL's "etag" query param, 81 | # then immediately give a 304 response. This prevents the overhead of 82 | # proxying requests for cached files. 83 | if options.headers?['if-none-match'] && localDigestMatch = url.match(/etag=([^&]+[^&]?)/) 84 | if options.headers['if-none-match'] is decodeURIComponent(localDigestMatch[1]) 85 | debug "∟ Intercepted the request based on its etag param, sending a 304 response".green 86 | res.writeHead(304, { 87 | server: 'vee' 88 | }) 89 | res.end() 90 | return 91 | 92 | req.pipe(request(options)).pipe res 93 | 94 | server = http.createServer() 95 | server.on 'request', handle 96 | 97 | if config.httpsPort?[0] 98 | # The key is included in the public git repo, this is in no way secure, it's to be used from 99 | # your local machine to your local machine 100 | httpsServer = https.createServer 101 | key: fs.readFileSync(config.sslKey or "#{ __dirname }/keys/vee.key") 102 | cert: fs.readFileSync(config.sslCert or "#{ __dirname }/keys/vee.crt") 103 | 104 | httpsServer.on 'request', handle 105 | 106 | lDomain = domain.create() 107 | lDomain.on 'error', (err) -> 108 | switch err?.code 109 | when 'EACCES' 110 | console.log "Permissions issue binding to port #{ config.httpPort } or #{ config.httpsPort }".red 111 | console.log "Perhaps you need to run vee as root? (sudo vee)" 112 | console.log "Or use the -p/-s flags to start vee on ports above 1024" 113 | process.exit(1) 114 | when 'EADDRINUSE' 115 | console.log "It seems that port #{ config.httpPort } or #{ config.httpsPort } is already in use".red 116 | console.log "Please terminate the processes bound to those ports, or use the -p/-s flags to start vee on different ports" 117 | process.exit(1) 118 | 119 | throw err 120 | 121 | lDomain.run -> 122 | console.log "Proxy starting:" 123 | console.log " http on #{ config.httpPort.join(', ') }".green 124 | 125 | for port in config.httpPort 126 | server.listen(port) 127 | 128 | if config.httpsPort?[0] 129 | console.log " https on #{ config.httpsPort.join(', ') }".green 130 | for port in config.httpsPort 131 | httpsServer.listen(port) 132 | 133 | stop = -> 134 | server?.close() 135 | httpsServer?.close() 136 | 137 | module.exports = {start, stop} 138 | --------------------------------------------------------------------------------