├── .gitignore ├── .editorconfig ├── package.json ├── .eslintrc ├── .travis.yml ├── LICENSE ├── lib └── Zuido.js ├── test └── zuido.spec.js ├── README.md └── cli.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zuido", 3 | "version": "0.3.3", 4 | "description": "", 5 | "bin": "cli.js", 6 | "scripts": { 7 | "test": "eslint cli.js && eslint lib/Zuido.js && mocha --exit" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "commander": "^2.20.3", 13 | "http": "0.0.0", 14 | "http-proxy": "^1.18.0", 15 | "http-proxy-response-rewrite": "0.0.1", 16 | "http-server": "^0.12.3", 17 | "ngrok": "^3.2.7", 18 | "opn": "^5.5.0" 19 | }, 20 | "devDependencies": { 21 | "chai": "^4.2.0", 22 | "eslint": "^4.19.1", 23 | "eslint-plugin-node": "^6.0.1", 24 | "mocha": "^5.2.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "es6": true, 5 | "node": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 2017 9 | }, 10 | "rules": { 11 | "arrow-body-style": "error", 12 | "arrow-parens": "error", 13 | "arrow-spacing": "error", 14 | "generator-star-spacing": "error", 15 | "no-duplicate-imports": "error", 16 | "no-useless-computed-key": "error", 17 | "no-useless-constructor": "error", 18 | "no-useless-rename": "error", 19 | "no-var": "error", 20 | "object-shorthand": "error", 21 | "prefer-arrow-callback": "error", 22 | "prefer-const": "error", 23 | "prefer-rest-params": "error", 24 | "prefer-spread": "error", 25 | "prefer-template": "error", 26 | "rest-spread-spacing": "error", 27 | "template-curly-spacing": "error", 28 | "yield-star-spacing": "error", 29 | "no-console": 0, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | notifications: 3 | email: 4 | on_success: never 5 | on_failure: change 6 | branches: 7 | only: 8 | - master 9 | - "/^v?[0-9\\.]+/" 10 | node_js: 11 | - '8' 12 | - '9' 13 | - '10' 14 | deploy: 15 | provider: npm 16 | email: hatatatehaze@gmail.com 17 | api_key: 18 | secure: hp1d7giPGm9kWFWVCX9mML/l7VB51n0ErcfdW16/PWwlkHosQ+AZ2VZHDkFJOXY3KszaYHNdNwgdLSX6iBeTB+i5ptHpYjcik9RQEzuwwlBzNYkEeF3fOPhhtT07fAt2S8lBBVjo5y2bjyFVvcrrGKaLDrm6j2QvwVdWe1J6tVW0W3WKnhXdnUu6eZsogdZbygKv9ouc6a4oj9Yb+LewtIOq5+hPZIUOCP+Uh/mitBIqz4msKoVUKrtR+WYeKfgidt7axn2j+7YjLygJcZJm8CHpVtGZVRaFgBMW+5ZoRCyxY0aKbHlFFutIOUppZsJcPz+7UFSfiKDVeRpkxHzkpqO7JL98NthIwK/4WzkATGV9hG5nB28HKvPVpBt7GxE0g17OIecQJ/xXzvJpDB9DBPJgvFZ6MH9BrYXwTr6Wh7M5Ixo3gFyvFzOgjVzDB9XCvh931MeRYMguUuodQOdnpVxfdS9Rzc1QAuKTrA6k/l1rW3aCeioVnQNovH+yL1GUiJ1pZeeAmBSqqHhI9ilGla15N9S6svQ87Lhb638lLtImrGEaEqo3l85Bl8/KikFP47iEsPiujeI7Ul0Ro+n8xDDr6dDDdZs/YhyRfyzJbByFpTcf1xujAPhNJUIv040A9yXsH+ALLSLox7PY2/Yqwf8cs86saDximAVaGUIaX9w= 19 | on: 20 | branch: master 21 | tags: true 22 | repo: miya0001/zuido 23 | skip_cleanup: true 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Takayuki Miyauchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/Zuido.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const url = require('url'); 4 | 5 | exports.getArgs = (program, callbak) => { 6 | const args = {}; 7 | 8 | if (program.httpServer) { 9 | program.args[0] = `http://127.0.0.1:${parseInt(program.httpServer)}` 10 | } 11 | 12 | if (program.args.length) { 13 | try { 14 | const origin = url.parse(program.args[0]); 15 | if (null === origin.protocol) { 16 | return callbak(); 17 | } else { 18 | args.origin = `${origin.protocol}//${origin.host}`; 19 | args.url = program.args[0]; 20 | args.regex = new RegExp(`https?://${origin.host}`, 'ig'); 21 | } 22 | } catch(e) { 23 | return callbak(); 24 | } 25 | } else { 26 | return callbak(); 27 | } 28 | 29 | if (program.subdomain) { 30 | args.subdomain = program.subdomain; 31 | } 32 | 33 | if (program.proxy) { 34 | args.proxy = program.proxy; 35 | } else { 36 | args.proxy = 5000; 37 | } 38 | 39 | if (program.region) { 40 | args.region = program.region; 41 | } 42 | 43 | if (program.config) { 44 | args.config = program.config; 45 | } else { 46 | args.config = `${process.env.HOME}/.ngrok2/ngrok.yml`; 47 | } 48 | 49 | return args; 50 | } 51 | 52 | exports.error = (message) => { 53 | console.log(`\u001b[31mError: ${message}\u001b[0m`); 54 | process.exit(1); 55 | } 56 | -------------------------------------------------------------------------------- /test/zuido.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const assert = chai.assert; 3 | const zuido = require('../lib/Zuido'); 4 | 5 | const program = {}; 6 | 7 | program.outputHelp = function() { 8 | return 'help'; 9 | } 10 | 11 | describe('`getArgs()` should work as expected.', function(){ 12 | it('should return error if illegal URL is passed.', function(){ 13 | program.args = ['test']; 14 | assert.deepEqual('error', zuido.getArgs(program, () => { 15 | return 'error' 16 | })); 17 | }); 18 | it('should return error if empty URL is passed.', function(){ 19 | program.args = ['']; 20 | assert.deepEqual('error', zuido.getArgs(program, () => { 21 | return 'error' 22 | })); 23 | }); 24 | it('should return default values.', function(){ 25 | program.args = ['http://localhost:8080/hello-world']; 26 | const args = zuido.getArgs(program, () => {}); 27 | assert.deepEqual('http://localhost:8080', args.origin); 28 | assert.deepEqual('http://localhost:8080/hello-world', args.url); 29 | assert.deepEqual('/https?:\\/\\/localhost:8080/gi', args.regex.toString()); 30 | assert.deepEqual(5000, args.proxy); 31 | assert.deepEqual(`${process.env.HOME}/.ngrok2/ngrok.yml`, args.config); 32 | }); 33 | it('should return values as expected.', function(){ 34 | program.args = ['http://localhost:8080/hello-world']; 35 | program.proxy = 3333; 36 | program.config = 'hello'; 37 | const args = zuido.getArgs(program, () => {}); 38 | assert.deepEqual('http://localhost:8080', args.origin); 39 | assert.deepEqual('http://localhost:8080/hello-world', args.url); 40 | assert.deepEqual('/https?:\\/\\/localhost:8080/gi', args.regex.toString()); 41 | assert.deepEqual(3333, args.proxy); 42 | assert.deepEqual('hello', args.config); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zuido 2 | 3 | [![Build Status](https://travis-ci.org/miya0001/zuido.svg?branch=master)](https://travis-ci.org/miya0001/zuido) 4 | [![npm version](https://badge.fury.io/js/zuido.svg)](https://badge.fury.io/js/zuido) 5 | 6 | `zuido` is a command line tool that allows you to connect your local development environment from public URLs with ngrok and simple reverse proxy. 7 | 8 | The proxy server in this command will change the URLs in your HTML to the public URL that is supplied by ngrok. 9 | 10 | https://ngrok.com/ 11 | 12 | Do you want to test your WordPress with mobile? Zuido allows you to do it with zero configuration. 13 | 14 | "zuido" means "tunnel" in Japanese. :) 15 | 16 | ## Requires 17 | 18 | * Node 8.x or later 19 | * macOS, Unix/Linux 20 | 21 | ## Usage 22 | 23 | ``` 24 | $ zuido --help 25 | 26 | Usage: zuido [options] 27 | 28 | Options: 29 | 30 | -V, --version output the version number 31 | --subdomain Custom subdomain. 32 | --region ngrok server region. [us, eu, au, ap] (default: us) 33 | --proxy The port number for the reverse proxy. 34 | --config Path to config files 35 | -h, --help output usage information 36 | ``` 37 | 38 | ## OPTIONS 39 | 40 | ### URL 41 | 42 | The URL like `http://localhost:8080`, `http://192.168.33.10/hello/world` or so. 43 | 44 | ``` 45 | $ zuido http://localhost:8080/ 46 | ``` 47 | 48 | ### subdomain 49 | 50 | Optional. subdomain name to request. If unspecified, uses the tunnel name. 51 | 52 | ``` 53 | $ zuido http://localhost:8080/ --subdomain=xxxx 54 | ``` 55 | 56 | Free plan user can't use subdomain. 57 | https://ngrok.com/pricing 58 | 59 | ### region 60 | 61 | Optional. The location of the datacenter for ngrok. The default value is `us`. 62 | 63 | * us - United States (Ohio) 64 | * eu - Europe (Frankfurt) 65 | * ap - Asia/Pacific (Singapore) 66 | * au - Australia (Sydney) 67 | 68 | ``` 69 | $ zuido http://localhost:8080/ --region=ap 70 | ``` 71 | 72 | ### proxy 73 | 74 | Optional. The port number for the reverse proxy in this command. The default value is `5000`. 75 | 76 | ``` 77 | $ zuido http://localhost:8080/ --proxy=3000 78 | ``` 79 | 80 | ### config 81 | 82 | Optional. Path to the config file for the ngrok. 83 | 84 | ``` 85 | $ zuido http://localhost:8080/ --config=/path/to/ngrok.yml 86 | ``` 87 | 88 | See documentation for ngrok. 89 | https://ngrok.com/docs#config 90 | 91 | ## Examples 92 | 93 | Forwards public URL (e.g, `https://xxxxxxxx.ngrok.io`) to `http://localhsot:8080` and open your browser. 94 | 95 | ``` 96 | $ zuido http://localhsot:8080 97 | ``` 98 | 99 | If you are payed user of the ngrok, you can choose ngrok's subdomain like following. 100 | 101 | ``` 102 | $ zuido --subdomain=zuido http://localhsot:8080 103 | ``` 104 | 105 | The proxy server on this command will run on port 5000, you can change the port. 106 | ``` 107 | $ zuido --proxy=3000 http://localhsot:8080 108 | ``` 109 | 110 | You can pass full url like following. 111 | 112 | ``` 113 | $ zuido http://example.com/path/to/app?hello=world 114 | ``` 115 | 116 | In this case, new URL will be `http://xxxxxxxx.ngrok.io/path/to/app?hello=world`. 117 | 118 | ## How to install 119 | 120 | ``` 121 | $ npm install -g zuido 122 | ``` 123 | 124 | ## Contributing 125 | 126 | ``` 127 | $ git clone git@github.com:miya0001/zuido.git 128 | $ cd zuido && npm install 129 | $ npm test 130 | ``` 131 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const http = require('http'); 6 | const httpProxy = require('http-proxy'); 7 | const modifyResponse = require('http-proxy-response-rewrite'); 8 | const ngrok = require('ngrok'); 9 | const opn = require('opn'); 10 | const fs = require('fs'); 11 | const program = require('commander'); 12 | const zuido = require('./lib/Zuido.js'); 13 | const pkg = require('./package.json'); 14 | const httpd = require('http-server'); 15 | 16 | program 17 | .version(pkg.version) 18 | .usage('[options] ') 19 | .option('--subdomain ', 'Custom subdomain.') 20 | .option('--region ', 'ngrok server region. [us, eu, au, ap] (default: us)') 21 | .option('--proxy ', 'The port number for the reverse proxy.', parseInt) 22 | .option('--http-server ', 'Launch a webserver with the specific port.', parseInt) 23 | .option('--config ', 'Path to config files') 24 | .parse(process.argv); 25 | 26 | const args = zuido.getArgs(program, () => { 27 | program.outputHelp(); 28 | process.exit(1); 29 | }); 30 | 31 | connectNgrok().then((client) => { 32 | if (parseInt(program.httpServer)) { 33 | const server = httpd.createServer({ 34 | root: "./", 35 | }) 36 | 37 | server.listen(parseInt(program.httpServer)); 38 | } 39 | 40 | const update_hostname = (str) => { 41 | if ('string' === typeof str) { 42 | return str.replace(args.regex, client.url.replace(/\/$/, '')) 43 | } else { 44 | return str; 45 | } 46 | } 47 | 48 | const proxy = httpProxy.createProxyServer({ 49 | target: args.origin, 50 | changeOrigin: true, 51 | secure: false, 52 | }); 53 | 54 | proxy.on('proxyRes', (proxyRes, req, res) => { 55 | if (proxyRes.headers['content-type'] && proxyRes.headers['content-type'].match(/text\/html/i)) { 56 | delete proxyRes.headers['content-length']; 57 | modifyResponse(res, proxyRes.headers['content-encoding'], (body) => { 58 | if (body) { 59 | body = update_hostname(body); 60 | } 61 | return body; 62 | }); 63 | } 64 | Object.keys(proxyRes.headers).forEach((key) => { 65 | proxyRes.headers[key] = update_hostname(proxyRes.headers[key]); 66 | }); 67 | }); 68 | 69 | http.createServer((req, res) => { 70 | try { 71 | proxy.web(req, res); 72 | } catch(e) { 73 | zuido.error('Please check URL and try again.'); 74 | } 75 | }).listen(args.proxy).on('error', () => { 76 | zuido.error(`Can not listen on port ${args.proxy}.`); 77 | }); 78 | 79 | console.log(`\u001b[36mzuido v${pkg.version}\u001b[0m by \u001b[36mTakayuki Miyauchi (@miya0001)`); 80 | console.log('\u001b[32mWeb Interface: \u001b[0m' + 'http://localhost:4040'); 81 | console.log(`\u001b[32mForwarding: \u001b[0m${client.url} -> ${args.origin}`); 82 | console.log(`\u001b[32mConfig Path: \u001b[0m${args.config}`); 83 | console.log('\u001b[0m(Ctrl+C to quit)') 84 | 85 | opn(update_hostname(args.url)); 86 | }).catch((error) => { 87 | console.log(error); 88 | process.exit(1); 89 | }); 90 | 91 | async function connectNgrok() { 92 | const client = {} 93 | const opts = { 94 | proto: 'http', 95 | addr: args.proxy, 96 | } 97 | 98 | try { 99 | fs.statSync(args.config); 100 | opts.configPath = args.config; 101 | } catch(err) { 102 | // nothing to do. 103 | } 104 | 105 | if (args.subdomain) { 106 | opts.subdomain = args.subdomain; 107 | } 108 | 109 | if (args.region) { 110 | opts.region = args.region; 111 | } 112 | 113 | try{ 114 | client.url = await ngrok.connect(opts); 115 | } catch(e) { 116 | zuido.error(e.details.err); 117 | process.exit(1); 118 | } 119 | 120 | client.opts = opts; 121 | 122 | return client; 123 | } 124 | --------------------------------------------------------------------------------