Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tall-gorillas-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"cloudflared": minor
---

Tunnel class with custom output parser
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ jobs:
- macos-latest
version:
- "latest"
- "2024.8.2"
- "2024.12.1"
- "2024.10.1"
- "2024.8.3"
- "2024.6.1"
- "2024.4.1"
- "2024.2.1"

name: "${{ matrix.os }} - ${{ matrix.version }}"
runs-on: ${{ matrix.os }}
Expand Down
48 changes: 16 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,29 +77,30 @@ spawn(bin, ["--version"], { stdio: "inherit" });

Checkout [`examples/tunnel.js`](examples/tunnel.js).

`Tunnel` is inherited from `EventEmitter`, so you can listen to the events it emits, checkout [`examples/events.mjs`](examples/events.mjs).

```js
import { tunnel } from "cloudflared";
import { Tunnel } from "cloudflared";

console.log("Cloudflared Tunnel Example.");
main();

async function main() {
// run: cloudflared tunnel --hello-world
const { url, connections, child, stop } = tunnel({ "--hello-world": null });
const tunnel = Tunnel.quick();

// show the url
const url = new Promise((resolve) => tunnel.once("url", resolve));
console.log("LINK:", await url);

// wait for the all 4 connections to be established
const conns = await Promise.all(connections);

// show the connections
console.log("Connections Ready!", conns);
// wait for connection to be established
const conn = new Promise((resolve) => tunnel.once("connected", resolve));
console.log("CONN:", await conn);

// stop the tunnel after 15 seconds
setTimeout(stop, 15_000);
setTimeout(tunnel.stop, 15_000);

child.on("exit", (code) => {
tunnel.on("exit", (code) => {
console.log("tunnel process exited with code", code);
});
}
Expand All @@ -108,29 +109,12 @@ async function main() {
```sh
❯ node examples/tunnel.js
Cloudflared Tunnel Example.
LINK: https://aimed-our-bite-brought.trycloudflare.com
Connections Ready! [
{
id: 'd4681cd9-217d-40e2-9e15-427f9fb77856',
ip: '198.41.200.23',
location: 'MIA'
},
{
id: 'b40d2cdd-0b99-4838-b1eb-9a58a6999123',
ip: '198.41.192.107',
location: 'LAX'
},
{
id: '55545211-3f63-4722-99f1-d5fea688dabf',
ip: '198.41.200.53',
location: 'MIA'
},
{
id: 'f3d5938a-d48c-463c-a4f7-a158782a0ddb',
ip: '198.41.192.77',
location: 'LAX'
}
]
LINK: https://mailto-davis-wilderness-facts.trycloudflare.com
CONN: {
id: 'df1b8330-44ea-4ecb-bb93-8a32400f6d1c',
ip: '198.41.200.193',
location: 'tpe01'
}
tunnel process exited with code 0
```

Expand Down
45 changes: 45 additions & 0 deletions examples/events.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Tunnel, ConfigHandler } from "cloudflared";

const token = process.env.CLOUDFLARED_TOKEN;
if (!token) {
throw new Error("CLOUDFLARED_TOKEN is not set");
}

const tunnel = Tunnel.withToken(token);
const handler = new ConfigHandler(tunnel);

handler.on("config", ({ config }) => {
console.log("Config", config);
});

tunnel.on("url", (url) => {
console.log("Tunnel is ready at", url);
});

tunnel.on("connected", (connection) => {
console.log("Connected to", connection);
});

tunnel.on("disconnected", (connection) => {
console.log("Disconnected from", connection);
});

tunnel.on("stdout", (data) => {
console.log("Tunnel stdout", data);
});

tunnel.on("stderr", (data) => {
console.error("Tunnel stderr", data);
});

tunnel.on("exit", (code, signal) => {
console.log("Tunnel exited with code", code, "and signal", signal);
});

tunnel.on("error", (error) => {
console.error("Error", error);
});

process.on("SIGINT", () => {
console.log("Tunnel stopped", tunnel.stop());
});
16 changes: 7 additions & 9 deletions examples/tunnel.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
const { tunnel } = require("cloudflared");
const { Tunnel } = require("cloudflared");

console.log("Cloudflared Tunnel Example.");
main();

async function main() {
// run: cloudflared tunnel --hello-world
const { url, connections, child, stop } = tunnel({ "--hello-world": null });
const tunnel = Tunnel.quick();

// show the url
const url = new Promise((resolve) => tunnel.once("url", resolve));
console.log("LINK:", await url);

// wait for the all 4 connections to be established
const conns = await Promise.all(connections);

// show the connections
console.log("Connections Ready!", conns);
const conn = new Promise((resolve) => tunnel.once("connected", resolve));
console.log("CONN:", await conn);

// stop the tunnel after 15 seconds
setTimeout(stop, 15_000);
setTimeout(tunnel.stop, 15_000);

child.on("exit", (code) => {
tunnel.on("exit", (code) => {
console.log("tunnel process exited with code", code);
});
}
16 changes: 7 additions & 9 deletions examples/tunnel.mjs
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import { tunnel } from "cloudflared";
import { Tunnel } from "cloudflared";

console.log("Cloudflared Tunnel Example.");
main();

async function main() {
// run: cloudflared tunnel --hello-world
const { url, connections, child, stop } = tunnel({ "--hello-world": null });
const tunnel = Tunnel.quick();

// show the url
const url = new Promise((resolve) => tunnel.once("url", resolve));
console.log("LINK:", await url);

// wait for the all 4 connections to be established
const conns = await Promise.all(connections);

// show the connections
console.log("Connections Ready!", conns);
const conn = new Promise((resolve) => tunnel.once("connected", resolve));
console.log("CONN:", await conn);

// stop the tunnel after 15 seconds
setTimeout(stop, 15_000);
setTimeout(tunnel.stop, 15_000);

child.on("exit", (code) => {
tunnel.on("exit", (code) => {
console.log("tunnel process exited with code", code);
});
}
83 changes: 39 additions & 44 deletions src/_tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { ChildProcess } from "node:child_process";
import fs from "node:fs";
import { bin, install, tunnel, service } from "../lib.js";
import { Tunnel, bin, install, service } from "../lib.js";
import { describe, it, expect, beforeAll } from "vitest";

process.env.VERBOSE = "1";

describe(
"install",
{ timeout: 60_000 },
() => {
it("should install binary", async () => {
if (fs.existsSync(bin)) {
Expand All @@ -18,61 +19,55 @@ describe(
expect(fs.existsSync(bin)).toBe(true);
});
},
{ timeout: 60_000 },
);

describe(
"tunnel",
{ timeout: 60_000 },
() => {
it("should create a tunnel", async () => {
const { url, connections, child, stop } = tunnel({
"--url": "localhost:8080",
"--no-autoupdate": "true",
});
const tunnel = new Tunnel(["tunnel", "--url", "localhost:8080", "--no-autoupdate"]);
const url = new Promise((resolve) => tunnel.once("url", resolve));
expect(await url).toMatch(/https?:\/\/[^\s]+/);
await connections[0]; // quick tunnel only has one connection
expect(child).toBeInstanceOf(ChildProcess);
stop();
const conn = new Promise((resolve) => tunnel.once("connected", resolve));
await conn; // quick tunnel only has one connection
expect(tunnel.process).toBeInstanceOf(ChildProcess);
tunnel.stop();
});
},
{ timeout: 60_000 },
);

describe(
"service",
() => {
const TOKEN = process.env.TUNNEL_TOKEN;
const should_run =
TOKEN &&
["darwin", "linux"].includes(process.platform) &&
!(process.platform === "linux" && process.getuid?.() !== 0);
if (should_run) {
beforeAll(() => {
if (service.exists()) {
service.uninstall();
}
});
}

it("should work", async (ctx) => {
if (!should_run) {
ctx.skip();
describe("service", { timeout: 60_000 }, () => {
const TOKEN = process.env.TUNNEL_TOKEN;
const should_run =
TOKEN &&
["darwin", "linux"].includes(process.platform) &&
!(process.platform === "linux" && process.getuid?.() !== 0);
if (should_run) {
beforeAll(() => {
if (service.exists()) {
service.uninstall();
}
expect(service.exists()).toBe(false);
service.install(TOKEN);
});
}

await new Promise((r) => setTimeout(r, 15_000));
it("should work", async (ctx) => {
if (!should_run) {
ctx.skip();
}
expect(service.exists()).toBe(false);
service.install(TOKEN);

expect(service.exists()).toBe(true);
const current = service.current();
expect(current.tunnelID.length).toBeGreaterThan(0);
expect(current.connectorID.length).toBeGreaterThan(0);
expect(current.connections.length).toBeGreaterThan(0);
expect(current.metrics.length).toBeGreaterThan(0);
expect(current.config.ingress?.length).toBeGreaterThan(0);
await new Promise((r) => setTimeout(r, 15_000));

service.uninstall();
});
},
{ timeout: 60_000 },
);
expect(service.exists()).toBe(true);
const current = service.current();
expect(current.tunnelID.length).toBeGreaterThan(0);
expect(current.connectorID.length).toBeGreaterThan(0);
expect(current.connections.length).toBeGreaterThan(0);
expect(current.metrics.length).toBeGreaterThan(0);
expect(current.config.ingress?.length).toBeGreaterThan(0);

service.uninstall();
});
});
Loading
Loading