diff --git a/.github/workflows/ngrok-pull.yaml b/.github/workflows/ngrok-pull.yaml new file mode 100644 index 00000000..6287798e --- /dev/null +++ b/.github/workflows/ngrok-pull.yaml @@ -0,0 +1,29 @@ +name: ngrok-pull +on: + pull_request: + paths: + - ngrok/** +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + sparse-checkout: ngrok + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 18.x + registry-url: https://registry.npmjs.org + - name: Install winglang + run: npm i -g winglang + - name: Install dependencies + run: npm install --include=dev + working-directory: ngrok + - name: Test + run: wing test + working-directory: ngrok + - name: Pack + run: wing pack + working-directory: ngrok diff --git a/.github/workflows/ngrok-release.yaml b/.github/workflows/ngrok-release.yaml new file mode 100644 index 00000000..d2846769 --- /dev/null +++ b/.github/workflows/ngrok-release.yaml @@ -0,0 +1,37 @@ +name: ngrok-release +on: + push: + branches: + - main + paths: + - ngrok/** +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + sparse-checkout: ngrok + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 18.x + registry-url: https://registry.npmjs.org + - name: Install winglang + run: npm i -g winglang + - name: Install dependencies + run: npm install --include=dev + working-directory: ngrok + - name: Test + run: wing test + working-directory: ngrok + - name: Pack + run: wing pack + working-directory: ngrok + - name: Publish + run: npm publish --access=public --registry https://registry.npmjs.org --tag + latest *.tgz + working-directory: ngrok + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/ngrok/.gitignore b/ngrok/.gitignore new file mode 100644 index 00000000..297fdef9 --- /dev/null +++ b/ngrok/.gitignore @@ -0,0 +1,2 @@ +target/ +node_modules/ diff --git a/ngrok/LICENSE b/ngrok/LICENSE new file mode 100644 index 00000000..a875f479 --- /dev/null +++ b/ngrok/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Wing + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/ngrok/README.md b/ngrok/README.md new file mode 100644 index 00000000..69de6554 --- /dev/null +++ b/ngrok/README.md @@ -0,0 +1,54 @@ +# Wing ngrok support + +This library can be used to create an [ngrok](https://ngrok.com) tunnel for local development that +forwards HTTP requests to a localhost endpoint. + +When compiled to the cloud, this resource is a no-op. + +## Prerequisites + +* [winglang](https://winglang.io). +* An [ngrok account](https://ngrok.com). +* `NGROK_AUTHTOKEN` should include the auth token for your ngrok user. + +## Installation + +```sh +npm i @winglibs/ngrok +``` + +## Usage + +Let's forward all requests that are sent to `eladb.ngrok.dev` to our `cloud.Api`. + +```js +bring ngrok; + +let api = new cloud.Api(); + +api.get("/", inflight () => { + return { + status: 200, + body: "hello ngrok!" + }; +}); + +let t = new ngrok.Tunnel(api.url, domain: "eladb.ngrok.dev"); + +new cloud.Function(inflight () => { + log("tunnel connected to {t.url}"); +}); +``` + +## Roadmap + +- [ ] Do not require the domain +- [ ] `onConnect()` + +## Maintainers + +- [@eladb](https://github.com/eladb) + +## License + +This library is licensed under the [MIT License](./LICENSE). diff --git a/ngrok/example.main.w b/ngrok/example.main.w new file mode 100644 index 00000000..48d620f1 --- /dev/null +++ b/ngrok/example.main.w @@ -0,0 +1,19 @@ +bring cloud; +bring "./ngrok.w" as ngrok; + +let api = new cloud.Api(); + +api.get("/", inflight () => { + return { + status: 200, + body: "boo1m!" + }; +}); + +let web = new cloud.Website(path: "./public"); + +let t = new ngrok.Tunnel(api.url, domain: "eladb.ngrok.dev"); + +new cloud.Function(inflight () => { + log(t.url); +}); \ No newline at end of file diff --git a/ngrok/ngrok.js b/ngrok/ngrok.js new file mode 100644 index 00000000..f4465667 --- /dev/null +++ b/ngrok/ngrok.js @@ -0,0 +1,16 @@ +const ngrok = require('@ngrok/ngrok'); + +async function main(url, domain) { + const x = await ngrok.forward({ addr: url, domain, authtoken_from_env: true, proto: "http" }); + return x.url(); +} + +const url = process.argv[2]; +const domain = process.argv[3]; + +main(url, domain) + .then((url) => process.stdout.write(url)) + .catch((e) => process.stderr.write(e.message)); + +process.on("SIGINT", () => ngrok.kill().then(() => process.exit(0))); +process.stdin.resume(); diff --git a/ngrok/ngrok.test.w b/ngrok/ngrok.test.w new file mode 100644 index 00000000..7fae35df --- /dev/null +++ b/ngrok/ngrok.test.w @@ -0,0 +1,33 @@ +bring cloud; +bring "./ngrok.w" as ngrok; + +let api = new cloud.Api(); + +api.get("/", inflight () => { + return { + body: "hello?", + status: 200 + }; +}); + +api.get("/world", inflight () => { + return { + body: "damn it!!", + status: 200 + }; +}); + +api.get("/uri", inflight () => { + return { + status: 200, + body: "hey uri" + }; +}); + +let w = new cloud.Website(path: "./public"); + +let external = new ngrok.Tunnel(api.url, domain: "eladbgithub.ngrok.dev"); + +new cloud.Function(inflight () => { + log("ready {external.domain}"); +}); \ No newline at end of file diff --git a/ngrok/ngrok.w b/ngrok/ngrok.w new file mode 100644 index 00000000..af40410b --- /dev/null +++ b/ngrok/ngrok.w @@ -0,0 +1,52 @@ +bring cloud; +bring util; +bring fs; +bring sim; + +interface ChildProcess { + inflight kill(): void; + inflight url(): str; +} + +pub struct NgrokProps { + domain: str?; +} + +pub class Tunnel { + pub url: str; + state: sim.State; + + new(url: str, props: NgrokProps?) { + this.state = new sim.State(); + this.url = this.state.token("url"); + + if !nodeof(this).app.isTestEnvironment { + if !util.tryEnv("NGROK_AUTHTOKEN")? { + throw "NGROK_AUTHTOKEN is not defined"; + } + + let s = new cloud.Service(inflight () => { + try { + let child = Tunnel.spawn("node", Array["./ngrok.js", url, props?.domain]); + log("ngrok: {child.url()} => {url}"); + this.state.set("url", child.url()); + return () => { + child.kill(); + }; + } catch e { + log("error: {e}"); + + // this is needed, the exception will cause any dependents to never be initialized + this.state.set("url", ""); + } + }); + + // no need to show the ugly details + nodeof(s).hidden = true; + nodeof(this.state).hidden = true; + } + } + + extern "./util.js" + static inflight spawn(cmd: str, args: Array, opts: Json?): ChildProcess; +} \ No newline at end of file diff --git a/ngrok/package-lock.json b/ngrok/package-lock.json new file mode 100644 index 00000000..367e2b52 --- /dev/null +++ b/ngrok/package-lock.json @@ -0,0 +1,231 @@ +{ + "name": "@winglibs/ngrok", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@winglibs/ngrok", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "@ngrok/ngrok": "^0.9.1" + } + }, + "node_modules/@ngrok/ngrok": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok/-/ngrok-0.9.1.tgz", + "integrity": "sha512-G9C+ih9aiaQ2MGia2TEpxObQiJAyZXETD9FgwDqUuAk6Hq0Unby7XogdAH2H6hWkTBCkEYdTtA6397Bd2Vr2dQ==", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@ngrok/ngrok-android-arm-eabi": "0.9.1", + "@ngrok/ngrok-android-arm64": "0.9.1", + "@ngrok/ngrok-darwin-arm64": "0.9.1", + "@ngrok/ngrok-darwin-universal": "0.9.1", + "@ngrok/ngrok-darwin-x64": "0.9.1", + "@ngrok/ngrok-freebsd-x64": "0.9.1", + "@ngrok/ngrok-linux-arm-gnueabihf": "0.9.1", + "@ngrok/ngrok-linux-arm64-gnu": "0.9.1", + "@ngrok/ngrok-linux-arm64-musl": "0.9.1", + "@ngrok/ngrok-linux-x64-gnu": "0.9.1", + "@ngrok/ngrok-linux-x64-musl": "0.9.1", + "@ngrok/ngrok-win32-ia32-msvc": "0.9.1", + "@ngrok/ngrok-win32-x64-msvc": "0.9.1" + } + }, + "node_modules/@ngrok/ngrok-android-arm-eabi": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-android-arm-eabi/-/ngrok-android-arm-eabi-0.9.1.tgz", + "integrity": "sha512-/kQdjpBEsYjeo7rya0nA0uv5btoCk9aj0qqO2zMyeOO3m2qRIMH9lveCe1mOYPP4nNljPSq6JYa9qCrt5yMXUw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-android-arm64": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-android-arm64/-/ngrok-android-arm64-0.9.1.tgz", + "integrity": "sha512-vtenZxSqLYHQoVQK5llQ0xaizLEmZpWQGv7OwbwSOhDD8YgnEeqZx+Rmbz4VwiLhAnwZ6MG08DFYkVeG5nI/Gg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-darwin-arm64": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-arm64/-/ngrok-darwin-arm64-0.9.1.tgz", + "integrity": "sha512-UGm8PE1WY/+4WnejpXWf8aqKMOaxLqMavU7C+j3HKoe7Bk/7mZsGafPkGHoczvZXx1L3nqCm6O3yN5YzzqfZUw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-darwin-universal": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-universal/-/ngrok-darwin-universal-0.9.1.tgz", + "integrity": "sha512-cfcRqpreLSFpYHDdrB+KMiZwRFylmbD5cE3V5J/Lb6C7Vmu5MdZRMdMN5qafvdmEx96tH+zR45GbD58+vobLyw==", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-darwin-x64": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-x64/-/ngrok-darwin-x64-0.9.1.tgz", + "integrity": "sha512-fURb7XTKh/RyB7vWeUHd/HntRKqQqGI3ZDhkgGS8bG6fllqj9lK5tlutinfU3dwE/R/g2LMauvscS3fqPdFaJA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-freebsd-x64": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-freebsd-x64/-/ngrok-freebsd-x64-0.9.1.tgz", + "integrity": "sha512-8ANg7z1Ob4G/Aa275c+HiEDR3UEP/QlqQN+HKCmoPODfwY9sIG5+ioviGYkLWMwZ6f1f1zFV8+ETV93mzboi4g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-linux-arm-gnueabihf": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm-gnueabihf/-/ngrok-linux-arm-gnueabihf-0.9.1.tgz", + "integrity": "sha512-p3iTPJCVoXdptO3Jvb/SQFwvHe1Hw+Y8xVnVi3dnQQHVHqZxK9ye+HjXH/AFTmEG5IZTlZoz2qHtpwCBWO5LLg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-linux-arm64-gnu": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm64-gnu/-/ngrok-linux-arm64-gnu-0.9.1.tgz", + "integrity": "sha512-8CKxPqZnAsZmTXZTuOEQVmc2nOWbRpV5cjAxpsDk5cqyY6HPN08XqnAX+8mhcFvjsGViTHnrmny0Fxwvf4g/MQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-linux-arm64-musl": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm64-musl/-/ngrok-linux-arm64-musl-0.9.1.tgz", + "integrity": "sha512-GaD2+PvLTEnQdALHtwWPpaSHK3YHfF2FnI8Ze6ohiXVVCKxYVGXAZ6EmMeVnv7+t/zxcHyTcS91REUX51IWCiQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-linux-x64-gnu": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-x64-gnu/-/ngrok-linux-x64-gnu-0.9.1.tgz", + "integrity": "sha512-Hh0KBIuFtNaaeZj3px1UlkXwqZpiCsvcyu7wQ4U1rKJTRw79Qh7Ulm78uJ0a3mmG7J0yokUnCkPGsrpLcj7fbQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-linux-x64-musl": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-x64-musl/-/ngrok-linux-x64-musl-0.9.1.tgz", + "integrity": "sha512-OBitwuCkedTyr92VF0T/hTYQ0Y+HDEUVrDWTVxkdaSbp5mTNscUVelrUYkVDYdFiHg+PXyF4qHJq+IlrqShoiQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-win32-ia32-msvc": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-win32-ia32-msvc/-/ngrok-win32-ia32-msvc-0.9.1.tgz", + "integrity": "sha512-oYtgsu5FdrSaLlpiJjmcD8wMdkd/QaoojKnTjimBgvTyq64e1WgjtycGk6JESO08gpbd/3p1MCx2FmzBjzsdOw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-win32-x64-msvc": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-win32-x64-msvc/-/ngrok-win32-x64-msvc-0.9.1.tgz", + "integrity": "sha512-98Qum+DOIr50FM9A+BrwHPNbcpk0S23uSPlzNTvARybUFYgp6UxVCvZ6P/anZNVDSS+F+mHoIokS5X3Liwka4Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/ngrok/package.json b/ngrok/package.json new file mode 100644 index 00000000..42f95f6d --- /dev/null +++ b/ngrok/package.json @@ -0,0 +1,13 @@ +{ + "name": "@winglibs/ngrok", + "version": "0.0.1", + "repository": { + "type": "git", + "url": "https://github.com/winglang/winglibs.git", + "directory": "ngrok" + }, + "license": "MIT", + "dependencies": { + "@ngrok/ngrok": "^0.9.1" + } +} diff --git a/ngrok/public/index.html b/ngrok/public/index.html new file mode 100644 index 00000000..bf776ed9 --- /dev/null +++ b/ngrok/public/index.html @@ -0,0 +1,3 @@ + + Hey Wingnuts! + \ No newline at end of file diff --git a/ngrok/util.js b/ngrok/util.js new file mode 100644 index 00000000..e7028dd8 --- /dev/null +++ b/ngrok/util.js @@ -0,0 +1,23 @@ +const { spawn } = require("child_process"); + +exports.spawn = async (cmd, args, opts) => { + const child = spawn(cmd, args, { stdio: "pipe", ...opts }); + return new Promise((resolve, reject) => { + + // route to `console.xxx` so it shows in wing console + child.stdout.on("data", data => resolve({ + url: () => data.toString(), + kill: async () => { + if (child.killed) { + return; + } + + child.kill(); + return new Promise(resolve => child.on("close", resolve)); + } + })) + + child.stderr.on("data", data => reject(new Error(data.toString()))); + }); +}; +