Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add accept and reject functions to upgrade hook interface for more explicit auth flow #90

Closed
wants to merge 7 commits into from
Closed
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
56 changes: 56 additions & 0 deletions examples/node/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { defineHooks } from "crossws";
import { createServer } from 'node:http'
import crossws from "crossws/adapters/node";
import { readFileSync } from "node:fs";

const ws = crossws({
hooks: defineHooks({
upgrade(req, socket) {
// if (!authorizedCheck(req)) {
socket.reject("Unauthorized")
// } else {
// socket.accept()

// }
},

open(peer) {
// console.log("[ws] open", peer);
peer.send({ user: "server", message: `Welcome ${peer}!` });
},

message(peer, message) {
// console.log("[ws] message", peer, message);
if (message.text().includes("ping")) {
peer.send({ user: "server", message: "pong" });
} else {
peer.send({ user: peer.toString(), message: message.toString() });
}
},

close(peer, event) {
// console.log("[ws] close", peer, event);
},

error(peer, error) {
// console.log("[ws] error", peer, error);
},
}),
});

const hostname = '127.0.0.1';
const port = 3000;

const index = readFileSync("./public/index.html");

const server = createServer((req, res) => {
console.log('hit');
res.writeHead(200);
res.end(index);
})

server.on("upgrade", ws.handleUpgrade);

server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
12 changes: 12 additions & 0 deletions examples/node/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "crossws-examples-node",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "node ./index.js"
},
"dependencies": {
"crossws": "workspace:*"
}
}
180 changes: 180 additions & 0 deletions examples/node/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<!doctype html>
<html lang="en" data-theme="dark">

<head>
<title>CrossWS Test Page</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
background-color: #1a1a1a;
}
</style>
<script type="module">
// https://github.com/vuejs/petite-vue
import {
createApp,
reactive,
nextTick,
} from "https://esm.sh/[email protected]";

let ws;

const store = reactive({
message: "",
messages: [],
});

const scroll = () => {
nextTick(() => {
const el = document.querySelector("#messages");
el.scrollTop = el.scrollHeight;
el.scrollTo({
top: el.scrollHeight,
behavior: "smooth",
});
});
};

const format = async () => {
for (const message of store.messages) {
if (!message._fmt && message.text.startsWith("{")) {
message._fmt = true;
const { codeToHtml } = await import("https://esm.sh/[email protected]");
const str = JSON.stringify(JSON.parse(message.text), null, 2);
message.formattedText = await codeToHtml(str, {
lang: "json",
theme: "dark-plus",
});
}
}
};

const log = (user, ...args) => {
console.log("[ws]", user, ...args);
store.messages.push({
text: args.join(" "),
formattedText: "",
user: user,
date: new Date().toLocaleString(),
});
scroll();
format();
};

const connect = async () => {
const isSecure = location.protocol === "https:";
const url = (isSecure ? "wss://" : "ws://") + location.host + "/_ws";
if (ws) {
log("ws", "Closing previous connection before reconnecting...");
ws.close();
clear();
}

log("ws", "Connecting to", url, "...");
ws = new WebSocket(url);

ws.addEventListener("message", async (event) => {
let data =
typeof event.data === "string" ? data : await event.data.text();
const { user = "system", message = "" } = data.startsWith("{")
? JSON.parse(data)
: { message: data };
log(
user,
typeof message === "string" ? message : JSON.stringify(message),
);
});

ws.addEventListener("error",(event) => {
console.log("ws", "error", event);
});

ws.addEventListener("close",(event) => {
console.log("ws", "close", event);
});

ws.addEventListener("open",(event) => {
log("ws", "Connected!");
})


};

const clear = () => {
store.messages.splice(0, store.messages.length);
log("system", "previous messages cleared");
};

const send = () => {
console.log("sending message...");
if (store.message) {
ws.send(store.message);
}
store.message = "";
};

const ping = () => {
log("ws", "Sending ping");
ws.send("ping");
};

createApp({
store,
send,
ping,
clear,
connect,
rand: Math.random(),
}).mount();

await connect();
</script>
</head>

<body class="h-screen flex flex-col justify-between">
<main v-scope="{}">
<!-- Messages -->
<div id="messages" class="flex-grow flex flex-col justify-end px-4 py-8">
<div class="flex items-center mb-4" v-for="message in store.messages">
<div class="flex flex-col">
<p class="text-gray-500 mb-1 text-xs ml-10">{{ message.user }}</p>
<div class="flex items-center">
<img
:src="'https://www.gravatar.com/avatar/' + encodeURIComponent(message.user + rand) + '?s=512&d=monsterid'"
alt="Avatar" class="w-8 h-8 rounded-full" />
<div class="ml-2 bg-gray-800 rounded-lg p-2">
<p v-if="message.formattedText" class="overflow-x-scroll" v-html="message.formattedText"></p>
<p v-else class="text-white">{{ message.text }}</p>
</div>
</div>
<p class="text-gray-500 mt-1 text-xs ml-10">{{ message.date }}</p>
</div>
</div>
</div>

<!-- Chatbox -->
<div class="bg-gray-800 px-4 py-2 flex items-center justify-between fixed bottom-0 w-full">
<div class="w-full min-w-6">
<input type="text" placeholder="Type your message..."
class="w-full rounded-l-lg px-4 py-2 bg-gray-700 text-white focus:outline-none focus:ring focus:border-blue-300"
@keydown.enter="send" v-model="store.message" />
</div>
<div class="flex">
<button class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4" @click="send">
Send
</button>
<button class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4" @click="ping">
Ping
</button>
<button class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4" @click="connect">
Reconnect
</button>
<button class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded-r-lg" @click="clear">
Clear
</button>
</div>
</div>
</main>
</body>

</html>
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
packages:
- "examples/*"

36 changes: 22 additions & 14 deletions src/adapters/bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { WebSocketHandler, ServerWebSocket, Server } from "bun";
import type { AdapterOptions, AdapterInstance } from "../adapter.ts";
import { toBufferLike } from "../utils.ts";
import { defineWebSocketAdapter, adapterUtils } from "../adapter.ts";
import { AdapterHookable } from "../hooks.ts";
import { AdapterHookable, formatRejection, Reasons } from "../hooks.ts";
import { Message } from "../message.ts";
import { Peer } from "../peer.ts";

Expand All @@ -13,7 +13,7 @@ export interface BunAdapter extends AdapterInstance {
handleUpgrade(req: Request, server: Server): Promise<Response | undefined>;
}

export interface BunOptions extends AdapterOptions {}
export interface BunOptions extends AdapterOptions { }

type ContextData = {
peer?: BunPeer;
Expand All @@ -31,20 +31,28 @@ export default defineWebSocketAdapter<BunAdapter, BunOptions>(
return {
...adapterUtils(peers),
async handleUpgrade(request, server) {
const res = await hooks.callHook("upgrade", request);
if (res instanceof Response) {
return res;
let response: Response | undefined;

/** Accept the Websocket upgrade request. */
function accept(params?: { headers?: HeadersInit }): void {
if (!server.upgrade(request, {
headers: params?.headers,
data: {
server,
request,
} satisfies ContextData,
})) {
response = new Response("Upgrade failed", { status: 500 });
};
}
const upgradeOK = server.upgrade(request, {
data: {
server,
request,
} satisfies ContextData,
headers: res?.headers,
});
if (!upgradeOK) {
return new Response("Upgrade failed", { status: 500 });

/** Reject the Websocket upgrade request */
function reject(reason: Reasons): void {
response = formatRejection({ reason, type: "Response" })
}

await hooks.callHook("upgrade", request, { accept, reject });
return response ?? new Response("Upgrade failed", { status: 500 });
},
websocket: {
message: (ws, message) => {
Expand Down
53 changes: 33 additions & 20 deletions src/adapters/cloudflare-durable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { AdapterOptions, AdapterInstance } from "../adapter.ts";
import type * as web from "../../types/web.ts";
import { toBufferLike } from "../utils.ts";
import { defineWebSocketAdapter, adapterUtils } from "../adapter.ts";
import { AdapterHookable } from "../hooks.ts";
import { AdapterHookable, formatRejection, Reasons } from "../hooks.ts";
import { Message } from "../message.ts";
import { Peer } from "../peer.ts";

Expand Down Expand Up @@ -31,27 +31,40 @@ export default defineWebSocketAdapter<
// placeholder
},
handleDurableUpgrade: async (obj, request) => {
const res = await hooks.callHook("upgrade", request as Request);
if (res instanceof Response) {
return res;
let response: Response | undefined;

async function accept(params?: { headers?: HeadersInit }): Promise<void> {
const pair = new WebSocketPair();
const client = pair[0];
const server = pair[1];
const peer = CloudflareDurablePeer._restore(
obj,
server as unknown as CF.WebSocket,
request,
);
peers.add(peer);
(obj as DurableObjectPub).ctx.acceptWebSocket(server);
await hooks.callHook("open", peer);
// eslint-disable-next-line unicorn/no-null
response = new Response(null, {
status: 101,
webSocket: client,
headers: params?.headers,
});
}
const pair = new WebSocketPair();
const client = pair[0];
const server = pair[1];
const peer = CloudflareDurablePeer._restore(
obj,
server as unknown as CF.WebSocket,
request,

function reject(reason: Reasons): void {
response = formatRejection({ reason, type: "Response" })
}

await hooks.callHook("upgrade", request as Request,
{
accept,
reject
}
);
peers.add(peer);
(obj as DurableObjectPub).ctx.acceptWebSocket(server);
await hooks.callHook("open", peer);
// eslint-disable-next-line unicorn/no-null
return new Response(null, {
status: 101,
webSocket: client,
headers: res?.headers,
});

return response ?? new Response("Upgrade failed", { status: 500 });
},
handleDurableMessage: async (obj, ws, message) => {
const peer = CloudflareDurablePeer._restore(obj, ws as CF.WebSocket);
Expand Down
Loading