├── .gitignore ├── proxy-example.gif ├── install_node.sh ├── package.json ├── README.md ├── loading.html ├── LICENSE └── proxy.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /proxy-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSignal/learn_preview-proxy/main/proxy-example.gif -------------------------------------------------------------------------------- /install_node.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Install Node.js 4 | export NVM_DIR= 5 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash 6 | export NVM_DIR="$HOME/.nvm" 7 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 8 | nvm --version 9 | nvm install 22.13.1 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn_preview-poxy", 3 | "version": "1.0.0", 4 | "description": "proxy for the CodeSignal Learn preview window", 5 | "main": "proxy.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/CodeSignal/learn_preview-poxy.git" 12 | }, 13 | "author": "Brian Genisio", 14 | "license": "SEE LICENCE IN LICENCE", 15 | "bugs": { 16 | "url": "https://github.com/CodeSignal/learn_preview-poxy/issues" 17 | }, 18 | "homepage": "https://github.com/CodeSignal/learn_preview-poxy#readme", 19 | "dependencies": { 20 | "http-proxy": "1.18.1", 21 | "ws": "8.18.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Preview Proxy 2 | 3 | A simple proxy server that shows a loading page until the target application is ready. 4 | 5 | ![Demo of the Preview Proxy](./proxy-example.gif) 6 | 7 | ## Features 8 | 9 | - Shows a customizable loading page until the target application signals readiness 10 | - Proxies all requests to a target port once ready 11 | - Configurable redirect path when application is ready 12 | - Customizable loading screen messages 13 | 14 | ## Usage 15 | 16 | ```bash 17 | npm install 18 | node proxy.js [options] 19 | ``` 20 | 21 | ### Options 22 | 23 | - `--server-port `: Set the port for the proxy server (default: 3000) 24 | - `--proxy-port `: Set the port for the target application (default: 3001) 25 | - `--redirect-path `: Set the path to redirect to when the application is ready (default: "/") 26 | - `--heading-message `: Set the heading text for the loading page (default: "Loading Application") 27 | - `--subheading-message `: Set the subheading text for the loading page (default: "Please wait while we initialize your session...") 28 | - `is-ready`: Start in the ready state and skip the loading page 29 | 30 | ### Installing Node.js 31 | Some environments require installing Node.js manually. Use the `install_node.sh` script to install the correct version of Node.js for this project. It will start by installing `nvm` and then installs Node.js. 32 | 33 | ### A common scenario: 34 | To use this proxy, simply clone the repo, install Node.js if necessary, and start the proxy. 35 | 36 | ```bash 37 | git clone --depth 1 --branch v0.0.1 https://github.com/codesignal/learn_preview-proxy.git > /dev/null 2>&1 38 | cd learn_preview-proxy 39 | bash ./install_node.sh 40 | source ~/.bashrc 41 | node proxy.js 42 | ``` 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /loading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 54 | 87 | 88 | 89 |
90 |
91 |

HEADING_MESSAGE

92 |

SUBHEADING_MESSAGE

93 |
94 | 95 | 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Elastic License 2.0 2 | 3 | URL: https://www.elastic.co/licensing/elastic-license 4 | 5 | ## Acceptance 6 | 7 | By using the software, you agree to all of the terms and conditions below. 8 | 9 | ## Copyright License 10 | 11 | The licensor grants you a non-exclusive, royalty-free, worldwide, 12 | non-sublicensable, non-transferable license to use, copy, distribute, make 13 | available, and prepare derivative works of the software, in each case subject to 14 | the limitations and conditions below. 15 | 16 | ## Limitations 17 | 18 | You may not provide the software to third parties as a hosted or managed 19 | service, where the service provides users with access to any substantial set of 20 | the features or functionality of the software. 21 | 22 | You may not move, change, disable, or circumvent the license key functionality 23 | in the software, and you may not remove or obscure any functionality in the 24 | software that is protected by the license key. 25 | 26 | You may not alter, remove, or obscure any licensing, copyright, or other notices 27 | of the licensor in the software. Any use of the licensor’s trademarks is subject 28 | to applicable law. 29 | 30 | ## Patents 31 | 32 | The licensor grants you a license, under any patent claims the licensor can 33 | license, or becomes able to license, to make, have made, use, sell, offer for 34 | sale, import and have imported the software, in each case subject to the 35 | limitations and conditions in this license. This license does not cover any 36 | patent claims that you cause to be infringed by modifications or additions to 37 | the software. If you or your company make any written claim that the software 38 | infringes or contributes to infringement of any patent, your patent license for 39 | the software granted under these terms ends immediately. If your company makes 40 | such a claim, your patent license ends immediately for work on behalf of your 41 | company. 42 | 43 | ## Notices 44 | 45 | You must ensure that anyone who gets a copy of any part of the software from you 46 | also gets a copy of these terms. 47 | 48 | If you modify the software, you must include in any modified copies of the 49 | software prominent notices stating that you have modified the software. 50 | 51 | ## No Other Rights 52 | 53 | These terms do not imply any licenses other than those expressly granted in 54 | these terms. 55 | 56 | ## Termination 57 | 58 | If you use the software in violation of these terms, such use is not licensed, 59 | and your licenses will automatically terminate. If the licensor provides you 60 | with a notice of your violation, and you cease all violation of this license no 61 | later than 30 days after you receive that notice, your licenses will be 62 | reinstated retroactively. However, if you violate these terms after such 63 | reinstatement, any additional violation of these terms will cause your licenses 64 | to terminate automatically and permanently. 65 | 66 | ## No Liability 67 | 68 | *As far as the law allows, the software comes as is, without any warranty or 69 | condition, and the licensor will not be liable to you for any damages arising 70 | out of these terms or the use or nature of the software, under any kind of 71 | legal claim.* 72 | 73 | ## Definitions 74 | 75 | The **licensor** is the entity offering these terms, and the **software** is the 76 | software the licensor makes available under these terms, including any portion 77 | of it. 78 | 79 | **you** refers to the individual or entity agreeing to these terms. 80 | 81 | **your company** is any legal entity, sole proprietorship, or other kind of 82 | organization that you work for, plus all organizations that have control over, 83 | are under the control of, or are under common control with that 84 | organization. **control** means ownership of substantially all the assets of an 85 | entity, or the power to direct its management and policies by vote, contract, or 86 | otherwise. Control can be direct or indirect. 87 | 88 | **your licenses** are all the licenses granted to you for the software under 89 | these terms. 90 | 91 | **use** means anything you do with the software requiring one of your licenses. 92 | 93 | **trademark** means trademarks, service marks, and similar rights. 94 | -------------------------------------------------------------------------------- /proxy.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const httpProxy = require('http-proxy'); 6 | const WebSocket = require('ws'); 7 | 8 | // Parse command line arguments 9 | function parseArgs() { 10 | const args = process.argv.slice(2); 11 | const result = { 12 | serverPort: 3000, 13 | proxyPort: 3001, 14 | defaultIsReady: false, 15 | heading: "Loading Application", 16 | subheading: "Please wait while we initialize your session...", 17 | defaultRedirect: "/", 18 | }; 19 | 20 | for (let i = 0; i < args.length; i++) { 21 | if (args[i] === '--server-port' && args[i + 1]) { 22 | result.serverPort = parseInt(args[i + 1], 10); 23 | i++; 24 | } else if (args[i] === '--proxy-port' && args[i + 1]) { 25 | result.proxyPort = parseInt(args[i + 1], 10); 26 | i++; 27 | } else if (args[i] === '--is-ready') { 28 | result.isReady = true; 29 | } else if (args[i] === "--heading-message") { 30 | result.heading = args[i + 1]; 31 | i++; 32 | } else if (args[i] === "--subheading-message") { 33 | result.subheading = args[i + 1]; 34 | i++; 35 | } else if (args[i] === "--redirect-path") { 36 | result.defaultRedirect = args[i + 1]; 37 | i++; 38 | } else if (args[i] === '--help') { 39 | console.log("This proxy will start off in the 'loading' state until it recieves a signal to start proxying"); 40 | console.log(" - you can activate proxy mode by sending a request to /_ready like this`curl localhost:3000/_ready`"); 41 | console.log(" - pass ?path=/login to force the page to redirect to a specific path (/login for example)"); 42 | console.log(""); 43 | console.log("usage: "); 44 | console.log(" [--server-port 3000] define the port this proxy receives requests from"); 45 | console.log(" [--proxy-port 3001] define the port the target application is listening on."); 46 | console.log(" [--heading-message heading] defines the heading in the loading screen"); 47 | console.log(" [--subheading-message subheading] defines the heading in the loading screen"); 48 | console.log(" [--redirect-path /login] defines the default redirect when the proxy is ready"); 49 | console.log(" [--is-ready] starts the proxy in proxy mode (not loading...)"); 50 | process.exit(0); 51 | } 52 | } 53 | 54 | return result; 55 | } 56 | 57 | const args = parseArgs(); 58 | let isReady = args.defaultIsReady; 59 | 60 | const proxy = httpProxy.createProxyServer({}); 61 | // Handle proxy errors 62 | proxy.on('error', (err, req, res) => { 63 | console.error('Proxy error:', err); 64 | res.writeHead(502, { 'Content-Type': 'text/plain' }); 65 | res.end('Proxy error: The target server may be down or unreachable'); 66 | }); 67 | 68 | const loadingPageTemplate = fs.readFileSync(path.join(__dirname, 'loading.html'), 'utf8'); 69 | const loadingPage = loadingPageTemplate 70 | .replace("HEADING_MESSAGE", args.heading) 71 | .replace("SUBHEADING_MESSAGE", args.subheading); 72 | 73 | // Create HTTP server with error handling 74 | const server = http.createServer((req, res) => { 75 | try { 76 | if (req.url.startsWith('/_ready')) { 77 | isReady = true; 78 | // Parse the URL to get query parameters 79 | const parsedUrl = url.parse(req.url, true); 80 | const path = parsedUrl.query.path || args.defaultRedirect; 81 | 82 | // Broadcast to all WebSocket clients that we're ready 83 | wss.clients.forEach((client) => { 84 | if (client.readyState === WebSocket.OPEN) { 85 | client.send(JSON.stringify({ type: 'ready', path })); 86 | } 87 | }); 88 | res.writeHead(200); 89 | res.end('Proxy mode activated'); 90 | return; 91 | } 92 | 93 | if (!isReady) { 94 | res.writeHead(200, { 'Content-Type': 'text/html' }); 95 | res.end(loadingPage); 96 | return; 97 | } 98 | 99 | const target = `http://localhost:${args.proxyPort}`; 100 | proxy.web(req, res, { target }); 101 | } catch (err) { 102 | console.error('Server error:', err); 103 | res.writeHead(500, { 'Content-Type': 'text/plain' }); 104 | res.end('Internal server error'); 105 | } 106 | }); 107 | 108 | // Handle server-level errors 109 | server.on('error', (err) => { 110 | console.error('Server error:', err); 111 | }); 112 | 113 | // Create WebSocket server with error handling 114 | const wss = new WebSocket.Server({ server }); 115 | 116 | // Handle WebSocket server errors 117 | wss.on('error', (err) => { 118 | console.error('WebSocket server error:', err); 119 | }); 120 | 121 | // Handle individual WebSocket connection errors 122 | wss.on('connection', (ws) => { 123 | ws.on('error', (err) => { 124 | console.error('WebSocket connection error:', err); 125 | }); 126 | }); 127 | 128 | // Start server 129 | server.listen(args.serverPort, (err) => { 130 | if (err) { 131 | console.error('Failed to start server:', err); 132 | process.exit(1); 133 | } 134 | console.log(`Server running on port ${args.serverPort}`); 135 | console.log(`Will proxy to port ${args.proxyPort} when ready`); 136 | }); 137 | 138 | // Handle uncaught exceptions and unhandled rejections 139 | process.on('uncaughtException', (err) => { 140 | console.error('Uncaught exception:', err); 141 | }); 142 | 143 | process.on('unhandledRejection', (err) => { 144 | console.error('Unhandled rejection:', err); 145 | }); 146 | --------------------------------------------------------------------------------