├── .gcloudignore ├── .gitignore ├── LICENSE ├── README.md ├── config.example.js ├── grab-ngrok.js ├── index.js ├── package.json └── success-msg.js /.gcloudignore: -------------------------------------------------------------------------------- 1 | .git 2 | LICENSE 3 | *.md 4 | config.example.js 5 | .gitignore 6 | yarn.lock 7 | node_modules 8 | *.sh 9 | *.bat -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.js 2 | node_modules 3 | yarn.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Joshua Tzucker 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 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, 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gcp-proxy-func 2 | Very simple Google Cloud Function that proxies requests using Express and `http-proxy-middleware`. You define the address that requests should be proxied to by filling out config.js (copy `config.example.js` to `config.js`, and then change the URL). 3 | 4 | # Using 5 | Fill in `config.js`, and then either zip up the files and upload via Gcloud admin, or use [the Gcloud CLI](https://cloud.google.com/sdk/gcloud/) and `npm run deploy`. Or see below if using this to proxy to Ngrok. 6 | 7 | # Ngrok 8 | If you want to use this to have a stable Google Cloud Function address that proxies requests to a dynamic Ngrok address ([see this blog post for details](https://joshuatz.com/posts/2019/using-google-cloud-functions-permanent-url-to-proxy-ngrok-requests/)), you can automatically redeploy the function with the correct Ngrok public URL after it has changed, by using `npm run ngrok-deploy`. 9 | 10 | This command will grab the Ngrok public URL via the localhost API, update config.js by using `fs` to write to the file, and then redeploy your GCloud function to point to the updated URL. -------------------------------------------------------------------------------- /config.example.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | destination: 'https://postb.in/123-456' 3 | } -------------------------------------------------------------------------------- /grab-ngrok.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const fs = require('fs'); 3 | const http = require('http'); 4 | 5 | const ngrokErr = new Error('Could not parse Ngrok response. Is Ngrok running? Is port 4040 being blocked?'); 6 | 7 | // Query ngrok local API 8 | const req = http.get('http://127.0.0.1:4040/api/tunnels', (res) => { 9 | 10 | let data = ''; 11 | res.on('data', (chunk) => { 12 | data += chunk; 13 | }); 14 | 15 | res.on('end', () => { 16 | // Extract public URL 17 | try { 18 | const ngrokTunnelInfo = JSON.parse(data); 19 | // Assume first tunnel 20 | const publicUrl = ngrokTunnelInfo.tunnels[0].public_url; 21 | // Update config.js file 22 | const configText = ` 23 | module.exports = { 24 | destination: '${publicUrl}' 25 | } 26 | `; 27 | fs.writeFileSync('./config.js', configText); 28 | console.log('config updated!'); 29 | return publicUrl; 30 | } catch (e) { 31 | throw ngrokErr; 32 | } 33 | }); 34 | }); 35 | 36 | req.on('error', (err) => { 37 | throw ngrokErr; 38 | }) -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const express = require('express'); 3 | const config = require('./config'); 4 | const proxy = require('http-proxy-middleware'); 5 | const bodyParser = require('body-parser'); 6 | const queryString = require('querystring'); 7 | 8 | // Create proxy instance 9 | const proxyInstance = getProxy(); 10 | 11 | const app = express(); 12 | app.use('/', proxyInstance); 13 | 14 | // Optional: Support special POST bodies - requires restreaming (see below) 15 | app.use(bodyParser.json()); 16 | app.use(bodyParser.urlencoded({extended: true})); 17 | 18 | function getProxy() { 19 | return proxy(getProxyConfig()); 20 | } 21 | 22 | /** 23 | * @returns {import('http-proxy-middleware').Config} 24 | */ 25 | function getProxyConfig() { 26 | return { 27 | target: config.destination, 28 | changeOrigin: true, 29 | followRedirects: true, 30 | secure: true, 31 | /** 32 | * @param {import('http').ClientRequest} proxyReq 33 | * @param {import('http').IncomingMessage} req 34 | * @param {import('http').ServerResponse} res 35 | */ 36 | onProxyReq: (proxyReq, req, res) => { 37 | /** 38 | * @type {null | undefined | object} 39 | */ 40 | // @ts-ignore 41 | const body = req.body; 42 | // Restream parsed body before proxying 43 | // https://github.com/http-party/node-http-proxy/blob/master/examples/middleware/bodyDecoder-middleware.js 44 | if (!body || !Object.keys(body).length) { 45 | return; 46 | } 47 | const contentType = proxyReq.getHeader('Content-Type'); 48 | let contentTypeStr = Array.isArray(contentType) ? contentType[0] : contentType.toString(); 49 | // Grab 'application/x-www-form-urlencoded' out of 'application/x-www-form-urlencoded; charset=utf-8' 50 | contentTypeStr = contentTypeStr.match(/^([^;]*)/)[1]; 51 | 52 | let bodyData; 53 | if (contentTypeStr === 'application/json') { 54 | bodyData = JSON.stringify(body); 55 | } 56 | 57 | if (contentTypeStr === 'application/x-www-form-urlencoded') { 58 | bodyData = queryString.stringify(body); 59 | } 60 | 61 | if (bodyData) { 62 | proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData)); 63 | proxyReq.write(bodyData); 64 | } 65 | } 66 | }; 67 | } 68 | 69 | module.exports = { 70 | proxy: app 71 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gcp-proxy-func", 3 | "version": "1.0.2", 4 | "description": "Google Cloud Function that proxies requests to a host specified in config.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "ngrok-deploy": "node grab-ngrok.js && npm run deploy", 9 | "deploy": "gcloud functions deploy proxy --runtime nodejs10 --trigger-http --memory=128 --timeout=60s && node success-msg.js" 10 | }, 11 | "keywords": [], 12 | "author": "Joshua Tzucker", 13 | "license": "MIT", 14 | "devDependencies": {}, 15 | "dependencies": { 16 | "body-parser": "^1.19.0", 17 | "express": "^4.17.1", 18 | "http-proxy-middleware": "^0.20.0", 19 | "querystring": "^0.2.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /success-msg.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const config = require('./config'); 3 | console.log(`Success! GCloud config updated to route traffic to ${config.destination}`); --------------------------------------------------------------------------------