Skip to content

Commit

Permalink
Export serve handler
Browse files Browse the repository at this point in the history
Signed-off-by: Marcos Candeia <[email protected]>
  • Loading branch information
mcandeia committed May 30, 2024
1 parent a07e69f commit d701f24
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 104 deletions.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ The Warp server opens a single HTTP port to which the Warp client connects and u

### Usage

To start the Warp server, import the `start` function from the Warp package and call it with the appropriate configuration.
To start the Warp server, import the `serve` function from the Warp package and call it with the appropriate configuration.

#### Example

```typescript
import { start } from "jsr:@deco/warp";
import { serve } from "jsr:@deco/warp";

const port = 8080; // The port where the Warp server will listen
const apiKeys = ["YOUR_API_KEY1", "YOUR_API_KEY2"]; // Array of API keys for authentication

start({ port, apiKeys });
serve({ port, apiKeys });
```

#### Parameters
Expand All @@ -47,13 +47,13 @@ import { connect } from "jsr:@deco/warp";
const port = 3000; // The local port you want to expose
const domain = "www.your.domain.com"; // The domain name for your service
const server = "wss://YOUR_SERVER"; // The WebSocket URL of your Warp server
const token = "YOUR_TOKEN"; // The authentication token
const apiKey = "YOUR_API_KEY"; // The apiKey

const { registered, closed } = await connect({
domain,
localAddr: `http://localhost:${port}`,
server,
token,
apiKey,
});

await registered;
Expand All @@ -69,7 +69,7 @@ closed.then(() => {
- `domain`: The domain name that will be used to access your localhost service.
- `localAddr`: The local address of the service you want to expose (e.g., `http://localhost:3000`).
- `server`: The WebSocket URL of your Warp server (e.g., `wss://YOUR_SERVER`).
- `token`: The authentication token for connecting to the Warp server.
- `apiKey`: The apiKey for connecting to the Warp server.

#### Return Values

Expand All @@ -83,12 +83,12 @@ Here’s a complete example of setting up a Warp server and client:
### Server

```typescript
import { start } from "jsr:@mcandeia/warp";
import { serve } from "jsr:@deco/warp";

const port = 8080;
const apiKeys = ["YOUR_API_KEY1", "YOUR_API_KEY2"];

start({ port, apiKeys });
serve({ port, apiKeys });
```

### Client
Expand All @@ -99,14 +99,14 @@ import { connect } from "jsr:@mcandeia/warp";
const port = 3000;
const domain = "www.your.domain.com";
const server = "wss://YOUR_SERVER";
const token = "YOUR_TOKEN";
const apiKey = "API_KEY";

(async () => {
const { registered, closed } = await connect({
domain,
localAddr: `http://localhost:${port}`,
server,
token,
apiKey,
});

await registered;
Expand Down
6 changes: 3 additions & 3 deletions client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import type { ClientMessage, ClientState, ServerMessage } from "./messages.ts";
/**
* Options for establishing a connection.
* @typedef {Object} ConnectOptions
* @property {string} token - The authentication token for connecting to the server.
* @property {string} apiKey - The apiKey used for connecting to the server.
* @property {string} domain - The domain to register the connection with.
* @property {string} server - The WebSocket server URL.
* @property {string} localAddr - The local address for the WebSocket connection.
*/
export interface ConnectOptions {
token: string;
apiKey: string;
domain: string;
server: string;
localAddr: string;
Expand Down Expand Up @@ -48,7 +48,7 @@ export const connect = async (opts: ConnectOptions): Promise<Connected> => {
await ch.out.send({
id: crypto.randomUUID(),
type: "register",
apiKey: opts.token,
apiKey: opts.apiKey,
domain: opts.domain,
});
const requestBody: Record<string, Channel<Uint8Array>> = {};
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@deco/warp",
"version": "0.1.3",
"version": "0.1.4",
"exports": "./mod.ts",
"tasks": {
"check": "deno fmt && deno lint && deno check mod.ts"
Expand Down
4 changes: 2 additions & 2 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { connect } from "./client.ts";
export type { ConnectOptions } from "./client.ts";
export { start } from "./server.ts";
export type { ServerOptions } from "./server.ts";
export { serve, serveHandler } from "./server.ts";
export type { HandlerOptions } from "./server.ts";

193 changes: 108 additions & 85 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,107 +22,130 @@ const ongoingRequests: Record<string, RequestObject> = {};
/**
* Represents options for configuring the server.
* @typedef {Object} ServerOptions
* @property {string[]} apiKeys - An array of API keys for authentication.
* @property {number} port - The port number where the server will listen for connections.
*/
export interface ServerOptions {
export interface ServeOptions extends HandlerOptions {
port?: number;
}

/**
* Represents options for configuring the server handler.
* @typedef {Object} HandlerOptions
* @property {string[]} apiKeys - An array of API keys for authentication.
* @property {string} connectPath - A path for connecting to the server.
*/
export interface HandlerOptions {
apiKeys: string[]
port: number;
connectPath?: string;
}

/**
* Starts the Warp server.
* @param {ServerOptions} [options] - Optional configurations for the server.
* @param {ServeOptions} [options] - Optional configurations for the server.
* @returns {Deno.HttpServer<Deno.NetAddr>} An instance of Deno HTTP server.
*/
export const start = (options?: ServerOptions): Deno.HttpServer<Deno.NetAddr> => {
const port = (options?.port ?? Deno.env.get("PORT"));
const apiKeys = options?.apiKeys ?? Deno.env.get("API_KEYS")?.split(",") ?? []; // array of api keys (random strings)

export const serve = (options: ServeOptions): Deno.HttpServer<Deno.NetAddr> => {
const port = (options?.port ?? 8000);
return Deno.serve({
handler: async (req) => {
const url = new URL(req.url);
if (url.pathname === "/_connect") {
const { socket, response } = Deno.upgradeWebSocket(req);
(async () => {
const ch = await makeWebSocket<ServerMessage, ClientMessage>(socket);
const state: ServerState = {
socket,
ch,
domainsToConnections,
ongoingRequests,
apiKeys,
}
for await (const message of ch.in.recv()) {
await handleClientMessage(state, message);
}
})()
return response;
handler: serveHandler(options),
port,
})
}

}
const host = req.headers.get("host");
if (host && host in domainsToConnections) {
const ch = domainsToConnections[host];
const messageId = crypto.randomUUID();
const hasBody = !!req.body;
const url = new URL(req.url);
const requestForward: ServerMessage = {
type: "request-start",
domain: host,
id: messageId,
method: req.method,
hasBody,
url: (url.pathname + url.search),
headers: [...req.headers.entries()].reduce((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {} as Record<string, string>),
};
/**
* Creates a handler function for serving requests, with support for WebSocket connections
* and forwarding requests to registered domains.
*
* @param {HandlerOptions} [options] - Optional configuration for the handler.
* @param {string[]} [options.apiKeys] - An array of API keys used for authentication.
* @param {string} [options.connectPath] - The path for WebSocket connection upgrades.
* @returns {(request: Request) => Response | Promise<Response>} - The request handler function.
*/
export const serveHandler = (options: HandlerOptions): (request: Request) => Response | Promise<Response> => {
const apiKeys = options.apiKeys; // array of api keys (random strings)
const connectPath = options?.connectPath ?? "/_connect";

// Create a writable stream using TransformStream
const responseObject = Promise.withResolvers<Response>();
ongoingRequests[messageId] = {
id: messageId,
requestObject: req,
responseObject,
dataChan: makeChan(),
return async (req) => {
const url = new URL(req.url);
if (url.pathname === connectPath) {
const { socket, response } = Deno.upgradeWebSocket(req);
(async () => {
const ch = await makeWebSocket<ServerMessage, ClientMessage>(socket);
const state: ServerState = {
socket,
ch,
domainsToConnections,
ongoingRequests,
apiKeys,
}
try {
const signal = ch.out.signal;
await ch.out.send(requestForward);
const dataChan = req.body ? makeChanStream(req.body) : undefined;
(async () => {
try {
for await (const chunk of dataChan?.recv(signal) ?? []) {
await ch.out.send({
type: "request-data",
id: messageId,
chunk,
});
}
if (signal.aborted) {
return;
}
for await (const message of ch.in.recv()) {
await handleClientMessage(state, message);
}
})()
return response;

}
const host = req.headers.get("host");
if (host && host in domainsToConnections) {
const ch = domainsToConnections[host];
const messageId = crypto.randomUUID();
const hasBody = !!req.body;
const url = new URL(req.url);
const requestForward: ServerMessage = {
type: "request-start",
domain: host,
id: messageId,
method: req.method,
hasBody,
url: (url.pathname + url.search),
headers: [...req.headers.entries()].reduce((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {} as Record<string, string>),
};

// Create a writable stream using TransformStream
const responseObject = Promise.withResolvers<Response>();
ongoingRequests[messageId] = {
id: messageId,
requestObject: req,
responseObject,
dataChan: makeChan(),
}
try {
const signal = ch.out.signal;
await ch.out.send(requestForward);
const dataChan = req.body ? makeChanStream(req.body) : undefined;
(async () => {
try {
for await (const chunk of dataChan?.recv(signal) ?? []) {
await ch.out.send({
type: "request-end",
type: "request-data",
id: messageId,
chunk,
});
} catch (err) {
responseObject.resolve(new Response("Error sending request to remote client", { status: 503 }));
if (signal.aborted) {
return;
}
console.log(`unexpected error when sending request`, err, req, messageId);
}
})()
return responseObject.promise;
} catch (err) {
console.error(new Date(), "Error sending request to remote client", err);
return new Response("Error sending request to remote client", { status: 503 });
}
if (signal.aborted) {
return;
}
await ch.out.send({
type: "request-end",
id: messageId,
});
} catch (err) {
responseObject.resolve(new Response("Error sending request to remote client", { status: 503 }));
if (signal.aborted) {
return;
}
console.log(`unexpected error when sending request`, err, req, messageId);
}
})()
return responseObject.promise;
} catch (err) {
console.error(new Date(), "Error sending request to remote client", err);
return new Response("Error sending request to remote client", { status: 503 });
}
return new Response("No registration for domain and/or remote service not available", { status: 503 });
},
port: port ? +port : 8000,
})
}
return new Response("No registration for domain and/or remote service not available", { status: 503 });
}
}
6 changes: 3 additions & 3 deletions test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { connect } from "./client.ts";
import { start } from "./server.ts";
import { serve } from "./server.ts";

const LOCAL_PORT = 8000;
const _localServer = Deno.serve({
Expand Down Expand Up @@ -32,7 +32,7 @@ const _localServer = Deno.serve({

const KEY = "c309424a-2dc4-46fe-bfc7-a7c10df59477";

const _tunnelServer = start({
const _tunnelServer = serve({
apiKeys: [KEY],
port: 8001
});
Expand All @@ -42,7 +42,7 @@ await connect({
domain,
localAddr: "http://localhost:8000",
server: "ws://localhost:8001",
token: KEY,
apiKey: KEY,
});


Expand Down

0 comments on commit d701f24

Please sign in to comment.