tickcli-js 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +360 -2
- package/client/game.js +982 -0
- package/client/game3d.js +109 -0
- package/client/relay.js +196 -0
- package/client/room.js +632 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +82 -0
- package/dist/cli.js.map +1 -0
- package/dist/hub.d.ts +619 -0
- package/dist/hub.js +1777 -0
- package/dist/hub.js.map +1 -0
- package/dist/node.d.ts +106 -0
- package/dist/node.js +301 -0
- package/dist/node.js.map +1 -0
- package/dist/remote.d.ts +75 -0
- package/dist/remote.js +214 -0
- package/dist/remote.js.map +1 -0
- package/dist/serve.d.ts +42 -0
- package/dist/serve.js +231 -0
- package/dist/serve.js.map +1 -0
- package/examples/games/arena.html +108 -0
- package/examples/games/arena3d.html +110 -0
- package/examples/games/chess.html +132 -0
- package/examples/games/cup.html +52 -0
- package/examples/games/delve.html +137 -0
- package/examples/games/dungeon.html +110 -0
- package/examples/games/neon-paint.html +79 -0
- package/examples/games/own-engine.html +41 -0
- package/examples/games/race.html +83 -0
- package/examples/games/shooter.html +95 -0
- package/examples/games/sky-duel.html +136 -0
- package/examples/games/space-squad.html +3627 -0
- package/examples/games/tag.html +51 -0
- package/examples/games/tictactoe.html +53 -0
- package/package.json +101 -4
- package/src/cli.ts +59 -0
- package/src/hub.ts +1521 -0
- package/src/node.ts +245 -0
- package/src/remote.ts +236 -0
- package/src/serve.ts +224 -0
package/dist/node.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"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"}
|
package/dist/remote.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import type { Duplex } from "node:stream";
|
|
3
|
+
import { RoomHub, type Limits, type SavePort } from "./hub.ts";
|
|
4
|
+
import { Heartbeat } from "./node.ts";
|
|
5
|
+
export interface Deployment {
|
|
6
|
+
/** public id, as in the page's script tag: letters, digits, "_" and "-", 3 to 64 */
|
|
7
|
+
id: string;
|
|
8
|
+
/** sites allowed to use it: "https://mygame.example", "https://mygame.example:8443", one level of wildcard subdomain
|
|
9
|
+
* ("https://*.mygame.example") or any port (":*"). http works only for localhost, 127.0.0.1 and [::1] ("http://localhost:*" to test from a
|
|
10
|
+
* laptop); an http entry for any other host never matches. */
|
|
11
|
+
origins: string[];
|
|
12
|
+
/** turned off: new sockets are refused (call revoke() to close its open rooms too) */
|
|
13
|
+
revoked?: boolean;
|
|
14
|
+
caps?: {
|
|
15
|
+
/** open rooms at once (default: the hub's maxRoomsPerScope) */
|
|
16
|
+
rooms?: number;
|
|
17
|
+
/** connected sockets at once */
|
|
18
|
+
players?: number;
|
|
19
|
+
/** data going out to its players, KB a second (over the last second); over it, new sockets wait (busy) and open rooms carry on */
|
|
20
|
+
kbPerSec?: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface RemoteOptions {
|
|
24
|
+
hub: RoomHub;
|
|
25
|
+
/** look a deployment up by id (your table, a JSON file); null or undefined = no such deployment */
|
|
26
|
+
deployment: (id: string) => Deployment | null | undefined | Promise<Deployment | null | undefined>;
|
|
27
|
+
heartbeat?: Heartbeat;
|
|
28
|
+
/** the client address after your trusted-proxy rule (default: the socket's address) */
|
|
29
|
+
address?: (req: http.IncomingMessage) => string;
|
|
30
|
+
/** socket path (default "/room") */
|
|
31
|
+
path?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface Remote {
|
|
34
|
+
/** For server.on("upgrade"): true when the request was for the room socket (handled), false to leave it to you. */
|
|
35
|
+
upgrade(req: http.IncomingMessage, socket: Duplex, head: Buffer): boolean;
|
|
36
|
+
/** For your request handler: serves /c/<id>.js and /client/<name>.js; true when it answered. */
|
|
37
|
+
request(req: http.IncomingMessage, res: http.ServerResponse): boolean;
|
|
38
|
+
/** Turn a deployment off now: its open rooms close (saves are written first) and its cached settings are dropped. */
|
|
39
|
+
revoke(id: string): void;
|
|
40
|
+
/** The hub namespace of a deployment (for hub.scopeStats, hub.closeScope). */
|
|
41
|
+
scope(id: string): string;
|
|
42
|
+
/** The rooms cap the hub should use for a namespace (pass to new RoomHub({ roomsPerScope })). */
|
|
43
|
+
roomsPerScope(scope: string): number | undefined;
|
|
44
|
+
/** connected sockets per deployment id */
|
|
45
|
+
sockets(id: string): number;
|
|
46
|
+
}
|
|
47
|
+
/** Is this page origin allowed for the deployment? An entry matches by scheme, host (exact, or "*." for one subdomain level) and port (exact,
|
|
48
|
+
* or ":*" for any). https sites only, except http on this machine (localhost, 127.0.0.1, [::1]) when listed. Never "null" or a file page. */
|
|
49
|
+
export declare function originAllowed(dep: Pick<Deployment, "origins">, origin: string | undefined): boolean;
|
|
50
|
+
/** The one-tag boot script for a deployment: relay.js, then the relay set up for this deployment, then room.js and game.js. */
|
|
51
|
+
export declare function bootScript(id: string, sources: {
|
|
52
|
+
relay: string;
|
|
53
|
+
room: string;
|
|
54
|
+
game: string;
|
|
55
|
+
}): string;
|
|
56
|
+
export declare function createRemote(o: RemoteOptions): Remote;
|
|
57
|
+
export interface RemoteServerOptions {
|
|
58
|
+
deployment: RemoteOptions["deployment"];
|
|
59
|
+
/** signs public player ids; keep it stable so ids stay the same across restarts (default: random per process) */
|
|
60
|
+
secret?: string;
|
|
61
|
+
/** a folder for saved profiles and worlds, or your own SavePort */
|
|
62
|
+
saves?: string | SavePort;
|
|
63
|
+
limits?: Partial<Limits>;
|
|
64
|
+
address?: RemoteOptions["address"];
|
|
65
|
+
/** behind a proxy on a private network: players' addresses from X-Forwarded-For (see clientAddress in tickcli-js/node) */
|
|
66
|
+
trustProxy?: boolean;
|
|
67
|
+
}
|
|
68
|
+
/** A complete remote room server: node:http + the hub + createRemote. Add your own routes on `server` before listen(). */
|
|
69
|
+
export declare function createRemoteServer(o: RemoteServerOptions): {
|
|
70
|
+
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
|
|
71
|
+
hub: RoomHub;
|
|
72
|
+
remote: Remote;
|
|
73
|
+
listen: (port?: number, host?: string) => Promise<string>;
|
|
74
|
+
close: () => Promise<void>;
|
|
75
|
+
};
|
package/dist/remote.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// tickcli-js/remote: games hosted anywhere (the author's own site, a portal, GitHub Pages) play in rooms on your tickcli server.
|
|
2
|
+
// The author pastes one tag; nothing else runs on their side:
|
|
3
|
+
//
|
|
4
|
+
// <script src="https://rooms.example/c/dep_ab12.js" data-game="tag"></script> <!-- relay + room.js + game.js, set up for deployment dep_ab12 -->
|
|
5
|
+
// <script> Game({ ... }) </script>
|
|
6
|
+
//
|
|
7
|
+
// A deployment is a public id with a list of the sites allowed to use it. The server checks the page's Origin at the socket upgrade (browsers
|
|
8
|
+
// send it and page scripts cannot change it), so another site cannot run its rooms on that deployment. One kind of deployment: https sites,
|
|
9
|
+
// plus http only for this machine when listed ("http://localhost:*"), so an author can test the same page from their laptop.
|
|
10
|
+
// Guests: the page keeps a random 128-bit key in its own storage and sends it as the socket's first message (no cookie, so it works from any
|
|
11
|
+
// site); the same key is the same seat. Each deployment is its own namespace (rooms and saves never mix) with optional caps on rooms,
|
|
12
|
+
// connected players and data out.
|
|
13
|
+
//
|
|
14
|
+
// What this does not do: a script outside a browser can send any Origin it likes. The caps, the hub's per-address limits and revoke() are
|
|
15
|
+
// the guard against that, not the origin list.
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import http from "node:http";
|
|
18
|
+
import crypto from "node:crypto";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { WebSocketServer } from "ws";
|
|
21
|
+
import { CLOSE, RoomHub } from "./hub.js";
|
|
22
|
+
import { GuestPasses, Heartbeat, WS_OPTIONS, attach, clientAddress, fileSaves, gameHash } from "./node.js";
|
|
23
|
+
const ID_RE = /^[A-Za-z0-9_-]{3,64}$/;
|
|
24
|
+
const GAME_RE = /^[A-Za-z0-9_-]{1,60}$/;
|
|
25
|
+
const VER_RE = /^[A-Za-z0-9._-]{1,40}$/;
|
|
26
|
+
const KEY_RE = /^[A-Za-z0-9_-]{22,64}$/;
|
|
27
|
+
const LOCAL = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
28
|
+
const CLIENT = fileURLToPath(new URL("../client/", import.meta.url));
|
|
29
|
+
/** Is this page origin allowed for the deployment? An entry matches by scheme, host (exact, or "*." for one subdomain level) and port (exact,
|
|
30
|
+
* or ":*" for any). https sites only, except http on this machine (localhost, 127.0.0.1, [::1]) when listed. Never "null" or a file page. */
|
|
31
|
+
export function originAllowed(dep, origin) {
|
|
32
|
+
if (!origin || origin === "null")
|
|
33
|
+
return false;
|
|
34
|
+
let u;
|
|
35
|
+
try {
|
|
36
|
+
u = new URL(origin);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (u.protocol !== "https:" && u.protocol !== "http:")
|
|
42
|
+
return false;
|
|
43
|
+
const host = u.hostname.toLowerCase();
|
|
44
|
+
if (u.protocol === "http:" && !LOCAL.has(host))
|
|
45
|
+
return false;
|
|
46
|
+
for (const raw of dep.origins) {
|
|
47
|
+
const m = /^(https?):\/\/(\*\.)?([a-z0-9.-]+|\[[0-9a-f:.]+\])(?::(\d{1,5}|\*))?\/?$/i.exec(raw.trim());
|
|
48
|
+
if (!m || `${m[1].toLowerCase()}:` !== u.protocol)
|
|
49
|
+
continue;
|
|
50
|
+
const want = m[3].toLowerCase();
|
|
51
|
+
if (m[2] ? !(host.endsWith(`.${want}`) && host.length > want.length + 1) : host !== want)
|
|
52
|
+
continue;
|
|
53
|
+
const port = m[4] === undefined ? "" : m[4] === "*" ? "*" : String(Number(m[4]) === (u.protocol === "https:" ? 443 : 80) ? "" : Number(m[4]));
|
|
54
|
+
if (port === "*" || port === u.port)
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
/** The one-tag boot script for a deployment: relay.js, then the relay set up for this deployment, then room.js and game.js. */
|
|
60
|
+
export function bootScript(id, sources) {
|
|
61
|
+
const boot = `;(function () {
|
|
62
|
+
var s = document.currentScript, d = (s && s.dataset) || {};
|
|
63
|
+
var base = new URL(s && s.src ? s.src : location.href);
|
|
64
|
+
var game = String(d.game || (location.pathname.split("/").pop() || "").replace(/\\.html?$/i, "") || "index").replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 60) || "index";
|
|
65
|
+
var ver = String(d.version || "1").replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 40) || "1";
|
|
66
|
+
var url = new URL("/room", base); url.protocol = base.protocol === "https:" ? "wss:" : "ws:";
|
|
67
|
+
url.searchParams.set("d", ${JSON.stringify(id)}); url.searchParams.set("game", game); url.searchParams.set("v", ver);
|
|
68
|
+
var mem = "";
|
|
69
|
+
function key() {
|
|
70
|
+
var k = "";
|
|
71
|
+
try { k = localStorage.getItem("tickcli.guest") || ""; } catch (e) { k = mem; }
|
|
72
|
+
if (!/^[A-Za-z0-9_-]{22,64}$/.test(k)) {
|
|
73
|
+
var b = new Uint8Array(16); crypto.getRandomValues(b);
|
|
74
|
+
k = btoa(String.fromCharCode.apply(null, Array.prototype.slice.call(b))).replace(/\\+/g, "-").replace(/\\//g, "_").replace(/=+$/, "");
|
|
75
|
+
try { localStorage.setItem("tickcli.guest", k); } catch (e) { mem = k; }
|
|
76
|
+
}
|
|
77
|
+
return k;
|
|
78
|
+
}
|
|
79
|
+
tickcliRelay({
|
|
80
|
+
frame: function () { return { contentWindow: window }; }, guest: true,
|
|
81
|
+
name: function () { try { return localStorage.getItem("tickcli.name") || ""; } catch (e) { return ""; } },
|
|
82
|
+
onName: function (n) { try { localStorage.setItem("tickcli.name", n); } catch (e) { /* private mode */ } },
|
|
83
|
+
connect: function () { return Promise.resolve({ url: url.href, first: { v: 1, t: "auth", ticket: key() } }); },
|
|
84
|
+
});
|
|
85
|
+
})();
|
|
86
|
+
`;
|
|
87
|
+
return `/* tickcli remote boot, deployment ${id}. MIT. */\n${sources.relay}\n${boot}\n${sources.room}\n;\n${sources.game}\n`;
|
|
88
|
+
}
|
|
89
|
+
export function createRemote(o) {
|
|
90
|
+
const path = o.path ?? "/room";
|
|
91
|
+
const wss = new WebSocketServer({ ...WS_OPTIONS });
|
|
92
|
+
const cache = new Map();
|
|
93
|
+
const live = new Map();
|
|
94
|
+
const src = (n) => fs.readFileSync(`${CLIENT}${n}.js`, "utf8");
|
|
95
|
+
const sources = { relay: src("relay"), room: src("room"), game: src("game") };
|
|
96
|
+
const files = { relay: sources.relay, room: sources.room, game: sources.game, game3d: src("game3d") };
|
|
97
|
+
const scope = (id) => `dep:${id}`;
|
|
98
|
+
const lookup = async (id) => {
|
|
99
|
+
if (!ID_RE.test(id))
|
|
100
|
+
return null;
|
|
101
|
+
let dep = null;
|
|
102
|
+
try {
|
|
103
|
+
dep = await o.deployment(id);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
dep = null;
|
|
107
|
+
}
|
|
108
|
+
if (dep && dep.id === id)
|
|
109
|
+
cache.set(id, dep);
|
|
110
|
+
else
|
|
111
|
+
cache.delete(id);
|
|
112
|
+
return dep && dep.id === id ? dep : null;
|
|
113
|
+
};
|
|
114
|
+
// refused sockets are still accepted, then closed with a code the page shows (4001 is final: the page stops trying; 1013 busy: it retries)
|
|
115
|
+
const closeWith = (req, socket, head, code, reason) => wss.handleUpgrade(req, socket, head, (ws) => ws.close(code, reason));
|
|
116
|
+
return {
|
|
117
|
+
scope,
|
|
118
|
+
sockets: (id) => live.get(id) || 0,
|
|
119
|
+
roomsPerScope: (sc) => (sc.startsWith("dep:") ? cache.get(sc.slice(4))?.caps?.rooms : undefined),
|
|
120
|
+
revoke(id) { cache.delete(id); o.hub.closeScope(scope(id), "this game was turned off by its owner"); },
|
|
121
|
+
upgrade(req, socket, head) {
|
|
122
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
123
|
+
if (url.pathname !== path)
|
|
124
|
+
return false;
|
|
125
|
+
const origin = req.headers.origin;
|
|
126
|
+
if (!origin) {
|
|
127
|
+
socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
const id = url.searchParams.get("d") || "", game = url.searchParams.get("game") || "", ver = url.searchParams.get("v") || "1";
|
|
131
|
+
if (!GAME_RE.test(game) || !VER_RE.test(ver)) {
|
|
132
|
+
closeWith(req, socket, head, CLOSE.badMessage, "bad game name or version");
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
socket.on("error", () => { });
|
|
136
|
+
void lookup(id).then((dep) => {
|
|
137
|
+
if (socket.destroyed)
|
|
138
|
+
return;
|
|
139
|
+
if (!dep || dep.revoked)
|
|
140
|
+
return closeWith(req, socket, head, CLOSE.notAllowed, "this game's deployment is unknown or turned off");
|
|
141
|
+
if (!originAllowed(dep, origin))
|
|
142
|
+
return closeWith(req, socket, head, CLOSE.notAllowed, `this site is not allowed for deployment ${id}`);
|
|
143
|
+
const caps = dep.caps || {}, sc = scope(id), n = live.get(id) || 0;
|
|
144
|
+
if (caps.players !== undefined && n >= caps.players)
|
|
145
|
+
return closeWith(req, socket, head, CLOSE.busy, "this game is full right now, trying again");
|
|
146
|
+
if (caps.kbPerSec !== undefined && o.hub.scopeStats(sc).bytesOut / 1024 > caps.kbPerSec)
|
|
147
|
+
return closeWith(req, socket, head, CLOSE.busy, "this game is busy right now, trying again");
|
|
148
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
149
|
+
live.set(id, (live.get(id) || 0) + 1);
|
|
150
|
+
ws.on("close", () => { const left = (live.get(id) || 1) - 1; if (left > 0)
|
|
151
|
+
live.set(id, left);
|
|
152
|
+
else
|
|
153
|
+
live.delete(id); });
|
|
154
|
+
attach(ws, {
|
|
155
|
+
hub: o.hub, heartbeat: o.heartbeat, address: o.address ? o.address(req) : req.socket.remoteAddress || "?",
|
|
156
|
+
// the guest key is the page's own random 128-bit value: unguessable, so nobody can take another player's seat; it names a seat in
|
|
157
|
+
// this deployment only (the hub shows others a short HMAC of it, never the key)
|
|
158
|
+
authenticate: (ticket) => typeof ticket === "string" && KEY_RE.test(ticket)
|
|
159
|
+
? { scope: sc, game: gameHash(`${game}|${ver}`), slot: game, who: { seat: `g:${ticket}`, name: GuestPasses.defaultName(ticket), guest: true } }
|
|
160
|
+
: null,
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
return true;
|
|
165
|
+
},
|
|
166
|
+
request(req, res) {
|
|
167
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
168
|
+
const js = (code, body, cache = "no-store") => {
|
|
169
|
+
res.writeHead(code, { "Content-Type": "text/javascript; charset=utf-8", "Cache-Control": cache, "X-Content-Type-Options": "nosniff", "Cross-Origin-Resource-Policy": "cross-origin", "Access-Control-Allow-Origin": "*" });
|
|
170
|
+
res.end(req.method === "HEAD" ? undefined : body);
|
|
171
|
+
};
|
|
172
|
+
if (req.method !== "GET" && req.method !== "HEAD")
|
|
173
|
+
return false;
|
|
174
|
+
const c = /^\/client\/(relay|room|game|game3d)\.js$/.exec(url.pathname);
|
|
175
|
+
if (c) {
|
|
176
|
+
js(200, files[c[1]], "public, max-age=300");
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
const b = /^\/c\/([^/]+)\.js$/.exec(url.pathname);
|
|
180
|
+
if (!b)
|
|
181
|
+
return false;
|
|
182
|
+
void lookup(b[1]).then((dep) => {
|
|
183
|
+
if (!dep || dep.revoked)
|
|
184
|
+
return js(404, `console.error(${JSON.stringify(`tickcli: no deployment ${b[1].slice(0, 64)} (unknown or turned off)`)});`);
|
|
185
|
+
js(200, bootScript(dep.id, sources), "public, max-age=300");
|
|
186
|
+
});
|
|
187
|
+
return true;
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** A complete remote room server: node:http + the hub + createRemote. Add your own routes on `server` before listen(). */
|
|
192
|
+
export function createRemoteServer(o) {
|
|
193
|
+
let remote = null;
|
|
194
|
+
const saves = typeof o.saves === "string" ? fileSaves(o.saves) : o.saves;
|
|
195
|
+
const hub = new RoomHub({ secret: o.secret ?? crypto.randomBytes(32).toString("base64url"), limits: o.limits, saves, roomsPerScope: (sc) => remote?.roomsPerScope(sc) });
|
|
196
|
+
hub.start();
|
|
197
|
+
const heartbeat = new Heartbeat();
|
|
198
|
+
remote = createRemote({ hub, deployment: o.deployment, heartbeat, address: o.address ?? (o.trustProxy ? (req) => clientAddress(req, true) : undefined) });
|
|
199
|
+
const server = http.createServer((req, res) => {
|
|
200
|
+
if (remote.request(req, res))
|
|
201
|
+
return;
|
|
202
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
203
|
+
res.end("not found");
|
|
204
|
+
});
|
|
205
|
+
server.on("upgrade", (req, socket, head) => { if (!remote.upgrade(req, socket, head))
|
|
206
|
+
socket.destroy(); });
|
|
207
|
+
const listen = (port = 8080, host) => new Promise((resolve, reject) => {
|
|
208
|
+
server.once("error", reject);
|
|
209
|
+
server.listen(port, host, () => { const a = server.address(); resolve(`http://${host && host !== "0.0.0.0" && host !== "::" ? host : "localhost"}:${typeof a === "object" && a ? a.port : port}/`); });
|
|
210
|
+
});
|
|
211
|
+
const close = async () => { const done = hub.shutdown(); heartbeat.stop(); server.close(); await done; };
|
|
212
|
+
return { server, hub, remote: remote, listen, close };
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=remote.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote.js","sourceRoot":"","sources":["../src/remote.ts"],"names":[],"mappings":"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"}
|
package/dist/serve.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { RoomHub, type Limits, type SavePort } from "./hub.ts";
|
|
3
|
+
export interface GameServerOptions {
|
|
4
|
+
/** Folder of games, one .html file each (names: letters, digits, "-" and "_"). Default: the current folder. */
|
|
5
|
+
games?: string;
|
|
6
|
+
/** Where saved profiles and worlds go: a folder (file saves) or your own SavePort. Default: none, a restart forgets them. */
|
|
7
|
+
saves?: string | SavePort;
|
|
8
|
+
/** Signs the players' pass cookies. Keep it the same across restarts so players keep their seats and saves. Default: random per process. */
|
|
9
|
+
secret?: string | Buffer;
|
|
10
|
+
/** Room limits (players, rooms, sizes, rates); see DEFAULT_LIMITS. */
|
|
11
|
+
limits?: Partial<Limits>;
|
|
12
|
+
/** Mark the pass cookie Secure (set this when the site is served over https). */
|
|
13
|
+
secure?: boolean;
|
|
14
|
+
/** Heading of the game list page. */
|
|
15
|
+
title?: string;
|
|
16
|
+
/** Behind a proxy on this machine or a private network (Caddy, nginx): take each player's address from X-Forwarded-For, so the
|
|
17
|
+
* per-address limits count players, not the proxy. Only believed from private or loopback peers. */
|
|
18
|
+
trustProxy?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface GameServer {
|
|
21
|
+
server: http.Server;
|
|
22
|
+
hub: RoomHub;
|
|
23
|
+
/** Starts listening; resolves with the address to open. */
|
|
24
|
+
listen(port?: number, host?: string): Promise<string>;
|
|
25
|
+
/** Stops the server; resolves once every save has been written. */
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/** three.js as one classic script that sets the global THREE, made from its ES module build (three.core.js + three.module.js: one export list,
|
|
29
|
+
* one import and one re-export from the core). Any other shape (a future three.js): null, so the page gets a clear 404 instead of a broken build. */
|
|
30
|
+
export declare function threeGlobal(dir: string): string | null;
|
|
31
|
+
/** A game page as a visitor gets it: its library tags pointed at /client/*.js and the relay running in the same window. */
|
|
32
|
+
export declare function playPage(html: string, game: string, roomPath?: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* A complete game server: every .html file in `games` gets live rooms, an invite link and anonymous players. Plain node:http + ws.
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* import { createGameServer } from "tickcli-js/serve";
|
|
38
|
+
* const app = createGameServer({ games: "./games", saves: "./saves" });
|
|
39
|
+
* await app.listen(8080); // http://localhost:8080/ lists the games
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare function createGameServer(o?: GameServerOptions): GameServer;
|
package/dist/serve.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// tickcli-js/serve: a complete game server in one call. Point it at a folder of .html games; every game gets live rooms, an invite link and
|
|
2
|
+
// anonymous players (a pass cookie names their seat). Plain Node (node:http + ws), no framework, no accounts.
|
|
3
|
+
//
|
|
4
|
+
// import { createGameServer } from "tickcli-js/serve";
|
|
5
|
+
// const app = createGameServer({ games: "./games", saves: "./saves" });
|
|
6
|
+
// await app.listen(8080); // http://localhost:8080/ lists the games
|
|
7
|
+
//
|
|
8
|
+
// A game page loads the libraries with any of: <script src="room.js">, <script src="lib:room">, <script src="tickcli/room.js"> (same for game.js
|
|
9
|
+
// and game3d.js). Engines installed next to tickcli load the same way: <script src="lib:three"></script> (global THREE, made from three's own
|
|
10
|
+
// ES module build at the first request, no bundler), lib:phaser (Phaser), lib:pixi (PIXI);
|
|
11
|
+
// the server points them at its own copies and runs the relay in the same window (the game is the top page, not a sandboxed frame).
|
|
12
|
+
// For signed-in players, your own routes or a sandboxed frame, build on RoomHub + tickcli-js/node instead (see the README, "Hosting it").
|
|
13
|
+
import crypto from "node:crypto";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import http from "node:http";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { createRequire } from "node:module";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { WebSocketServer } from "ws";
|
|
20
|
+
import { RoomHub } from "./hub.js";
|
|
21
|
+
import { GuestPasses, Heartbeat, WS_OPTIONS, attach, clientAddress, fileSaves, gameHash, sameSite } from "./node.js";
|
|
22
|
+
const CLIENT = fileURLToPath(new URL("../client/", import.meta.url));
|
|
23
|
+
const PASS = "cw_pass";
|
|
24
|
+
const SCOPE = "play";
|
|
25
|
+
const NAME_RE = /^[\w-]{1,60}\.html$/;
|
|
26
|
+
/** <script src="..."> forms a game may use for the two page libraries. */
|
|
27
|
+
const LIB_TAG = /<script\s+src=["'](?:lib:|(?:\.\/)?(?:tickcli(?:-js)?\/(?:client\/)?)?)(room|game|game3d)(?:\.js)?["']\s*>\s*<\/script>/gi;
|
|
28
|
+
/** Game engines a page may load with <script src="lib:NAME"></script> when their package is installed: package, its browser build (one classic
|
|
29
|
+
* script that sets a global; three.js has none, so it is made from its ES module build). */
|
|
30
|
+
const ENGINES = {
|
|
31
|
+
three: { pkg: "three", files: [] },
|
|
32
|
+
phaser: { pkg: "phaser", files: ["dist/phaser.min.js"] },
|
|
33
|
+
// PixiJS compiles its shaders with new Function unless its unsafe-eval add-on is there; game pages run under a CSP without eval
|
|
34
|
+
pixi: { pkg: "pixi.js", files: ["dist/pixi.min.js", "dist/packages/unsafe-eval.min.js"] },
|
|
35
|
+
};
|
|
36
|
+
const ENGINE_TAG = /<script\s+src=["']lib:(three|phaser|pixi)(?:\.js)?["']\s*>\s*<\/script>/gi;
|
|
37
|
+
/** An installed package's own folder (from this package or the folder the server runs in), or null. */
|
|
38
|
+
function pkgDir(name) {
|
|
39
|
+
for (const from of [import.meta.url, path.join(process.cwd(), "x.js")]) {
|
|
40
|
+
let main;
|
|
41
|
+
try {
|
|
42
|
+
main = createRequire(from).resolve(name);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
for (let d = path.dirname(main); d !== path.dirname(d); d = path.dirname(d)) {
|
|
48
|
+
try {
|
|
49
|
+
if (JSON.parse(fs.readFileSync(path.join(d, "package.json"), "utf8")).name === name)
|
|
50
|
+
return d;
|
|
51
|
+
}
|
|
52
|
+
catch { /* keep looking up */ }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/** The browser build of an engine, or null when it is not installed (or three.js has an unknown shape). Read once. */
|
|
58
|
+
const engineCache = new Map();
|
|
59
|
+
function engineSource(name) {
|
|
60
|
+
if (!engineCache.has(name)) {
|
|
61
|
+
const e = ENGINES[name], dir = e ? pkgDir(e.pkg) : null;
|
|
62
|
+
let src = null;
|
|
63
|
+
if (dir)
|
|
64
|
+
src = name === "three" ? threeGlobal(path.join(dir, "build"))
|
|
65
|
+
: e.files.every((f) => fs.existsSync(path.join(dir, f))) ? e.files.map((f) => fs.readFileSync(path.join(dir, f), "utf8").replace(/^\/\/# sourceMappingURL=.*$/gm, "")).join("\n;\n") : null;
|
|
66
|
+
engineCache.set(name, src);
|
|
67
|
+
}
|
|
68
|
+
return engineCache.get(name);
|
|
69
|
+
}
|
|
70
|
+
/** "A, B as C" from an export or import list: [[local name, exported name]]. */
|
|
71
|
+
function names(list) {
|
|
72
|
+
return list.split(",").map((x) => x.trim()).filter(Boolean).map((x) => { const m = /^(\S+)\s+as\s+(\S+)$/.exec(x); return m ? [m[1], m[2]] : [x, x]; });
|
|
73
|
+
}
|
|
74
|
+
/** three.js as one classic script that sets the global THREE, made from its ES module build (three.core.js + three.module.js: one export list,
|
|
75
|
+
* one import and one re-export from the core). Any other shape (a future three.js): null, so the page gets a clear 404 instead of a broken build. */
|
|
76
|
+
export function threeGlobal(dir) {
|
|
77
|
+
const read = (f) => { try {
|
|
78
|
+
return fs.readFileSync(path.join(dir, f), "utf8");
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
} };
|
|
83
|
+
let core = read("three.core.js"), mod = read("three.module.js");
|
|
84
|
+
if (core === null || mod === null)
|
|
85
|
+
return null;
|
|
86
|
+
const CORE = "['\"]\\./three\\.core\\.js['\"]";
|
|
87
|
+
const coreExp = new RegExp("^export \\{([^}]*)\\};?[ \\t]*$", "m").exec(core);
|
|
88
|
+
if (!coreExp)
|
|
89
|
+
return null;
|
|
90
|
+
core = core.replace(coreExp[0], "");
|
|
91
|
+
const imp = new RegExp(`^import \\{([^}]*)\\} from ${CORE};?[ \\t]*$`, "m").exec(mod);
|
|
92
|
+
const reExp = new RegExp(`^export \\{([^}]*)\\} from ${CORE};?[ \\t]*$`, "m").exec(mod);
|
|
93
|
+
if (!imp || !reExp)
|
|
94
|
+
return null;
|
|
95
|
+
mod = mod.replace(imp[0], "").replace(reExp[0], "");
|
|
96
|
+
const ownExp = new RegExp("^export \\{([^}]*)\\};?[ \\t]*$", "m").exec(mod);
|
|
97
|
+
if (!ownExp)
|
|
98
|
+
return null;
|
|
99
|
+
mod = mod.replace(ownExp[0], "");
|
|
100
|
+
if (/^\s*(import|export)\b/m.test(core) || /^\s*(import|export)\b/m.test(mod) || /\bimport\.meta\b|\bimport\s*\(/.test(core + mod))
|
|
101
|
+
return null;
|
|
102
|
+
const obj = (pairs, from) => pairs.map(([l, e]) => `${JSON.stringify(e)}: ${from(l)}`).join(",\n");
|
|
103
|
+
return `/* three.js (MIT, three.js authors) as the global THREE: its ES module build wrapped by tickcli, nothing changed inside */\n` +
|
|
104
|
+
`var THREE = (function () {\n"use strict";\nvar __core = (function () {\n${core}\nreturn {\n${obj(names(coreExp[1]), (l) => l)}\n};\n})();\n` +
|
|
105
|
+
`return (function () {\nvar {${names(imp[1]).map(([l, e]) => `${JSON.stringify(l)}: ${e}`).join(", ")}} = __core;\n${mod}\n` +
|
|
106
|
+
`return Object.freeze({\n${obj(names(reExp[1]), (l) => `__core[${JSON.stringify(l)}]`)},\n${obj(names(ownExp[1]), (l) => l)}\n});\n})();\n})();\n`;
|
|
107
|
+
}
|
|
108
|
+
/** The page's own socket address for its CSP (connect-src): only this server, as ws: and wss:. Plain 'self' covers same-origin sockets only in
|
|
109
|
+
* newer browsers, and "ws: wss:" alone allowed any host. A Host header that is not a plain host[:port] adds nothing. */
|
|
110
|
+
const wsOrigins = (req) => {
|
|
111
|
+
const h = String(req.headers.host || "");
|
|
112
|
+
return /^[a-z0-9.-]{1,253}(:\d{1,5})?$/i.test(h) || /^\[[0-9a-f:.]{2,45}\](:\d{1,5})?$/i.test(h) ? ` ws://${h} wss://${h}` : "";
|
|
113
|
+
};
|
|
114
|
+
const esc = (s) => s.replace(/[&<>"]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
115
|
+
const cookie = (req, name) => (req.headers.cookie || "").split(/;\s*/).map((p) => p.split("=")).find(([k]) => k === name)?.[1];
|
|
116
|
+
/** A game page as a visitor gets it: its library tags pointed at /client/*.js and the relay running in the same window. */
|
|
117
|
+
export function playPage(html, game, roomPath = "/room") {
|
|
118
|
+
const boot = `<script src="/client/relay.js"></script><script>tickcliRelay({frame:function(){return{contentWindow:window}},guest:true,` +
|
|
119
|
+
`name:function(){try{return localStorage.getItem("tickcli.name")||localStorage.getItem("hr.name")||""}catch(e){return ""}},onName:function(n){try{localStorage.setItem("tickcli.name",n);localStorage.removeItem("hr.name")}catch(e){}},` +
|
|
120
|
+
`connect:function(){return Promise.resolve({url:${JSON.stringify(`${roomPath}?game=${encodeURIComponent(game)}`)}})}})</script>`;
|
|
121
|
+
const out = html.replace(LIB_TAG, (_m, n) => `<script src="/client/${n.toLowerCase()}.js"></script>`).replace(ENGINE_TAG, (_m, n) => `<script src="/vendor/${n.toLowerCase()}.js"></script>`);
|
|
122
|
+
if (/<head[^>]*>/i.test(out))
|
|
123
|
+
return out.replace(/<head[^>]*>/i, (h) => h + boot);
|
|
124
|
+
// no <head>: right after the doctype, so the page stays in standards mode
|
|
125
|
+
return /^\s*<!doctype[^>]*>/i.test(out) ? out.replace(/^\s*<!doctype[^>]*>/i, (d) => d + boot) : boot + out;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A complete game server: every .html file in `games` gets live rooms, an invite link and anonymous players. Plain node:http + ws.
|
|
129
|
+
*
|
|
130
|
+
* ```ts
|
|
131
|
+
* import { createGameServer } from "tickcli-js/serve";
|
|
132
|
+
* const app = createGameServer({ games: "./games", saves: "./saves" });
|
|
133
|
+
* await app.listen(8080); // http://localhost:8080/ lists the games
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export function createGameServer(o = {}) {
|
|
137
|
+
const secret = o.secret === undefined ? crypto.randomBytes(32) : Buffer.from(o.secret);
|
|
138
|
+
const games = path.resolve(o.games ?? ".");
|
|
139
|
+
if (!fs.existsSync(games) || !fs.statSync(games).isDirectory())
|
|
140
|
+
throw new Error(`tickcli: no games folder at ${games}`);
|
|
141
|
+
const saves = typeof o.saves === "string" ? fileSaves(path.resolve(o.saves)) : o.saves;
|
|
142
|
+
const hub = new RoomHub({ secret: secret.toString("base64url"), limits: o.limits, saves });
|
|
143
|
+
hub.start();
|
|
144
|
+
const passes = new GuestPasses(secret);
|
|
145
|
+
const heartbeat = new Heartbeat();
|
|
146
|
+
const wss = new WebSocketServer({ ...WS_OPTIONS });
|
|
147
|
+
// A game is read and hashed once per version of its file (mtime + size), not on every page load and socket: after a restart every player
|
|
148
|
+
// reconnects within seconds, and a big game read and hashed 2,000 times would hold up the event loop.
|
|
149
|
+
const cache = new Map();
|
|
150
|
+
const read = (name) => {
|
|
151
|
+
if (!NAME_RE.test(name))
|
|
152
|
+
return null;
|
|
153
|
+
const file = path.join(games, name);
|
|
154
|
+
let st;
|
|
155
|
+
try {
|
|
156
|
+
st = fs.statSync(file);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
cache.delete(name);
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
if (!st.isFile())
|
|
163
|
+
return null;
|
|
164
|
+
const at = `${st.mtimeMs}|${st.size}`;
|
|
165
|
+
const hit = cache.get(name);
|
|
166
|
+
if (hit && hit.at === at)
|
|
167
|
+
return hit;
|
|
168
|
+
const html = fs.readFileSync(file, "utf8");
|
|
169
|
+
const got = { at, html, hash: gameHash(html) };
|
|
170
|
+
cache.set(name, got);
|
|
171
|
+
return got;
|
|
172
|
+
};
|
|
173
|
+
const title = o.title ?? "tickcli games";
|
|
174
|
+
const server = http.createServer((req, res) => {
|
|
175
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
176
|
+
const send = (code, type, body, headers = {}) => { res.writeHead(code, { "Content-Type": type, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", ...headers }); res.end(body); };
|
|
177
|
+
if (req.method !== "GET" && req.method !== "HEAD")
|
|
178
|
+
return send(405, "text/plain", "method not allowed");
|
|
179
|
+
if (url.pathname === "/") {
|
|
180
|
+
const list = fs.readdirSync(games).filter((f) => NAME_RE.test(f)).sort().map((f) => `<li><a href="/play/${f}">${esc(f.replace(/\.html$/, ""))}</a></li>`).join("");
|
|
181
|
+
return send(200, "text/html; charset=utf-8", `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc(title)}</title><h1>${esc(title)}</h1><ul>${list || "<li>No .html games in this folder yet.</li>"}</ul><p>Open a game in two windows (or send the link) to play together.</p>`);
|
|
182
|
+
}
|
|
183
|
+
const c = /^\/client\/(room|relay|game|game3d)\.js$/.exec(url.pathname);
|
|
184
|
+
if (c)
|
|
185
|
+
return send(200, "text/javascript; charset=utf-8", fs.readFileSync(path.join(CLIENT, `${c[1]}.js`), "utf8"));
|
|
186
|
+
const v = /^\/vendor\/(three|phaser|pixi)\.js$/.exec(url.pathname);
|
|
187
|
+
if (v) {
|
|
188
|
+
const src = engineSource(v[1]);
|
|
189
|
+
if (!src)
|
|
190
|
+
return send(404, "text/javascript; charset=utf-8", `console.error(${JSON.stringify(`lib:${v[1]}: install it next to tickcli (npm install ${ENGINES[v[1]].pkg})`)})`);
|
|
191
|
+
return send(200, "text/javascript; charset=utf-8", src, { "Cache-Control": "public, max-age=3600" });
|
|
192
|
+
}
|
|
193
|
+
const g = /^\/play\/([^/]+)$/.exec(url.pathname);
|
|
194
|
+
const html = g ? read(g[1])?.html : null;
|
|
195
|
+
if (!html)
|
|
196
|
+
return send(404, "text/plain", "not found");
|
|
197
|
+
// An anonymous pass on first visit (scripts cannot read it; it only names a seat, it proves nothing about a person).
|
|
198
|
+
const headers = { "Content-Security-Policy": `default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; connect-src 'self'${wsOrigins(req)}; frame-ancestors 'none'` };
|
|
199
|
+
if (!passes.check(SCOPE, cookie(req, PASS)))
|
|
200
|
+
headers["Set-Cookie"] = GuestPasses.cookie(PASS, passes.issue(SCOPE), "/", !!o.secure);
|
|
201
|
+
return send(200, "text/html; charset=utf-8", playPage(html, g[1]), headers);
|
|
202
|
+
});
|
|
203
|
+
server.on("upgrade", (req, socket, head) => {
|
|
204
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
205
|
+
const refuse = (code) => { socket.end(`HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`); };
|
|
206
|
+
if (url.pathname !== "/room")
|
|
207
|
+
return refuse(404);
|
|
208
|
+
if (!sameSite((n) => req.headers[n]))
|
|
209
|
+
return refuse(403);
|
|
210
|
+
const pass = passes.check(SCOPE, cookie(req, PASS));
|
|
211
|
+
if (!pass)
|
|
212
|
+
return refuse(403);
|
|
213
|
+
const name = url.searchParams.get("game") || "";
|
|
214
|
+
const game = read(name);
|
|
215
|
+
if (!game)
|
|
216
|
+
return refuse(404);
|
|
217
|
+
const identity = { scope: `${SCOPE}:${name}`, game: game.hash, slot: name, who: { seat: `g:${pass}`, name: GuestPasses.defaultName(pass), guest: true } };
|
|
218
|
+
wss.handleUpgrade(req, socket, head, (ws) => attach(ws, { hub, heartbeat, address: clientAddress(req, !!o.trustProxy), identity }));
|
|
219
|
+
});
|
|
220
|
+
const listen = (port = 8080, host) => new Promise((resolve, reject) => {
|
|
221
|
+
server.once("error", reject);
|
|
222
|
+
server.listen(port, host, () => {
|
|
223
|
+
const a = server.address();
|
|
224
|
+
const p = typeof a === "object" && a ? a.port : port;
|
|
225
|
+
resolve(`http://${!host || host === "0.0.0.0" || host === "::" ? "localhost" : host}:${p}/`);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
const close = async () => { const done = hub.shutdown(); heartbeat.stop(); wss.close(); server.close(); await done; };
|
|
229
|
+
return { server, hub, listen, close };
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=serve.js.map
|