openlimiter 1.2.0 → 1.3.1
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/README.md +45 -16
- package/dist/bin.js +53 -4
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts +110 -11
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1064 -64
- package/dist/cli.js.map +1 -1
- package/dist/codex-device-login.d.ts +87 -0
- package/dist/codex-device-login.d.ts.map +1 -0
- package/dist/codex-device-login.js +291 -0
- package/dist/codex-device-login.js.map +1 -0
- package/dist/config.d.ts +44 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +129 -7
- package/dist/config.js.map +1 -1
- package/dist/hub-auth.d.ts +137 -0
- package/dist/hub-auth.d.ts.map +1 -0
- package/dist/hub-auth.js +280 -0
- package/dist/hub-auth.js.map +1 -0
- package/dist/hub-sync.d.ts +144 -0
- package/dist/hub-sync.d.ts.map +1 -0
- package/dist/hub-sync.js +370 -0
- package/dist/hub-sync.js.map +1 -0
- package/dist/hub.d.ts +60 -0
- package/dist/hub.d.ts.map +1 -0
- package/dist/hub.js +178 -0
- package/dist/hub.js.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/ingest.d.ts +26 -0
- package/dist/ingest.d.ts.map +1 -1
- package/dist/ingest.js +116 -1
- package/dist/ingest.js.map +1 -1
- package/dist/qr.d.ts.map +1 -1
- package/dist/qr.js +1 -0
- package/dist/qr.js.map +1 -1
- package/dist/session.d.ts +46 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +121 -0
- package/dist/session.js.map +1 -0
- package/dist/statusline.d.ts +14 -0
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +258 -19
- package/dist/statusline.js.map +1 -1
- package/dist/terminal.d.ts +109 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +974 -0
- package/dist/terminal.js.map +1 -0
- package/package.json +4 -4
- package/dist/serve.d.ts +0 -170
- package/dist/serve.d.ts.map +0 -1
- package/dist/serve.js +0 -533
- package/dist/serve.js.map +0 -1
package/dist/serve.js
DELETED
|
@@ -1,533 +0,0 @@
|
|
|
1
|
-
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { createServer } from "node:http";
|
|
3
|
-
import { isIP } from "node:net";
|
|
4
|
-
import { networkInterfaces } from "node:os";
|
|
5
|
-
import { PROVIDER_CODES, buildAdvice, canonicalJson, floorFixed, readSnapshotCache } from "@openlimiter/core";
|
|
6
|
-
import { encodeQr, renderQr } from "./qr.js";
|
|
7
|
-
/**
|
|
8
|
-
* The local quota server.
|
|
9
|
-
*
|
|
10
|
-
* This is the one place OpenLimiter opens a socket, and it is a listener, never
|
|
11
|
-
* a client. It answers on your own network so a phone in your hand can read the
|
|
12
|
-
* same numbers the statusline shows on the machine that is actually doing the
|
|
13
|
-
* work. Nothing is uploaded, no account exists, and no provider is contacted.
|
|
14
|
-
*
|
|
15
|
-
* It is read only by construction. There is no route that writes, deletes, or
|
|
16
|
-
* mutates anything, and the whole server never touches a credential store, a
|
|
17
|
-
* provider authentication file, or a raw provider payload. The only thing it
|
|
18
|
-
* can say is the bounded set of fields the agent context already carries:
|
|
19
|
-
* provider codes, freshness states, a percentage, and a reset instant.
|
|
20
|
-
*
|
|
21
|
-
* Four gates stand between the socket and that payload:
|
|
22
|
-
*
|
|
23
|
-
* 1. A random token is generated on every start and is required on every
|
|
24
|
-
* route that carries data. It lives only in memory, is never written to
|
|
25
|
-
* disk, and is void the moment the command stops. The empty page shell at
|
|
26
|
-
* the root is the one route served without it, because the token travels
|
|
27
|
-
* in the URL fragment and a fragment never reaches a server. The shell
|
|
28
|
-
* holds no quota state, no token and no account detail: it is markup that
|
|
29
|
-
* then has to authenticate before anything is filled in.
|
|
30
|
-
* 2. The Host header must be an address literal or localhost, which is what
|
|
31
|
-
* closes a DNS rebinding attack from a page on an attacker's domain.
|
|
32
|
-
* 3. Cross origin requests are refused outright. The page this server hands
|
|
33
|
-
* out is same origin, so nothing legitimate needs a relaxation.
|
|
34
|
-
* 4. Only GET and HEAD are answered. Every other method is refused before
|
|
35
|
-
* the token is even looked at.
|
|
36
|
-
*
|
|
37
|
-
* It is meant for a trusted home or office network. It is not exposed to the
|
|
38
|
-
* internet, it opens no ports on a router, and it should not be reachable from
|
|
39
|
-
* one. Anyone already on your network who also has the token can read your
|
|
40
|
-
* quota state, which is the whole point and also the whole risk.
|
|
41
|
-
*/
|
|
42
|
-
/** Default listening port. Unassigned by IANA, and easy to type. */
|
|
43
|
-
export const DEFAULT_SERVE_PORT = 7317;
|
|
44
|
-
/** Default binding. Every interface, so a phone on the same network reaches it. */
|
|
45
|
-
export const DEFAULT_SERVE_HOST = "0.0.0.0";
|
|
46
|
-
/** Payload schema version. Bumped only on a breaking shape change. */
|
|
47
|
-
export const SERVE_SCHEMA = 1;
|
|
48
|
-
/**
|
|
49
|
-
* Name the token travels under. Short, because it goes in a QR symbol.
|
|
50
|
-
*
|
|
51
|
-
* It is the key in the URL FRAGMENT of every address this command prints, and
|
|
52
|
-
* for one more release it is also accepted as a query parameter so an address
|
|
53
|
-
* saved from an older build keeps working.
|
|
54
|
-
*/
|
|
55
|
-
export const TOKEN_PARAMETER = "t";
|
|
56
|
-
/**
|
|
57
|
-
* Why the token is in the fragment rather than the query.
|
|
58
|
-
*
|
|
59
|
-
* A query string is part of the request line. It lands in the phone's browser
|
|
60
|
-
* history, in the omnibox suggestions, in whatever cloud that browser syncs
|
|
61
|
-
* history to, and in the Referer of any link the page might carry. A fragment
|
|
62
|
-
* is never sent to a server at all: it stays in the address bar until the page
|
|
63
|
-
* itself reads it, and the page's first act is to move it into sessionStorage
|
|
64
|
-
* and rewrite the address to a clean one. From that point the token exists only
|
|
65
|
-
* in the tab's own memory and is sent as an Authorization header, so no URL
|
|
66
|
-
* anywhere on the phone carries the capability.
|
|
67
|
-
*
|
|
68
|
-
* A pairing link is a credential in a URL, so it gets the same treatment as one.
|
|
69
|
-
*/
|
|
70
|
-
export const TOKEN_FRAGMENT_PREFIX = "#" + TOKEN_PARAMETER + "=";
|
|
71
|
-
/** Key the page keeps the token under, inside its own tab and nowhere else. */
|
|
72
|
-
const TOKEN_STORAGE_KEY = "openlimiter.token";
|
|
73
|
-
/** Longest request target this server will look at. */
|
|
74
|
-
const MAX_TARGET_LENGTH = 512;
|
|
75
|
-
/** Bounds on the socket, so a stalled client cannot hold a slot forever. */
|
|
76
|
-
const HEADERS_TIMEOUT_MILLISECONDS = 5_000;
|
|
77
|
-
const REQUEST_TIMEOUT_MILLISECONDS = 10_000;
|
|
78
|
-
const KEEP_ALIVE_TIMEOUT_MILLISECONDS = 5_000;
|
|
79
|
-
const MAX_CONNECTIONS = 32;
|
|
80
|
-
/** Bytes of entropy in the token. */
|
|
81
|
-
const TOKEN_BYTES = 16;
|
|
82
|
-
/**
|
|
83
|
-
* The address another device on the same network should open.
|
|
84
|
-
*
|
|
85
|
-
* A bound address of every interface tells a phone nothing, so the first non
|
|
86
|
-
* internal IPv4 address is what gets printed. IPv6 is skipped deliberately: a
|
|
87
|
-
* link local address needs a zone identifier that does not survive a QR symbol.
|
|
88
|
-
*/
|
|
89
|
-
export function lanAddress() {
|
|
90
|
-
for (const entries of Object.values(networkInterfaces())) {
|
|
91
|
-
for (const entry of entries ?? []) {
|
|
92
|
-
if (entry.family === "IPv4" && !entry.internal)
|
|
93
|
-
return entry.address;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
function authority(host, port) {
|
|
99
|
-
const wrapped = isIP(host) === 6 ? "[" + host + "]" : host;
|
|
100
|
-
return wrapped + ":" + String(port);
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Decide whether the Host header may be served.
|
|
104
|
-
*
|
|
105
|
-
* A DNS rebinding attack works by pointing a name the attacker controls at an
|
|
106
|
-
* address on your network and letting a page on that name talk to a server
|
|
107
|
-
* there. The name always arrives in the Host header, so refusing every name
|
|
108
|
-
* except localhost closes it. Address literals are accepted because they
|
|
109
|
-
* cannot be rebound.
|
|
110
|
-
*/
|
|
111
|
-
export function isAllowedHostHeader(header, boundPort) {
|
|
112
|
-
if (header === undefined || header === "")
|
|
113
|
-
return false;
|
|
114
|
-
let parsed;
|
|
115
|
-
try {
|
|
116
|
-
parsed = new URL("http://" + header);
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
if (parsed.pathname !== "/" || parsed.search !== "" || parsed.username !== "") {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
const port = parsed.port === "" ? 80 : Number(parsed.port);
|
|
125
|
-
if (port !== boundPort)
|
|
126
|
-
return false;
|
|
127
|
-
const hostname = parsed.hostname;
|
|
128
|
-
if (hostname === "localhost")
|
|
129
|
-
return true;
|
|
130
|
-
/* A bracketed IPv6 literal loses its brackets through the URL parser. */
|
|
131
|
-
const literal = hostname.startsWith("[") ? hostname.slice(1, -1) : hostname;
|
|
132
|
-
return isIP(literal) !== 0;
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Read the token a request presents.
|
|
136
|
-
*
|
|
137
|
-
* The header is the supported way and is read first. The query parameter is a
|
|
138
|
-
* fallback kept for one release so an address copied out of an older build
|
|
139
|
-
* still answers; nothing this command prints uses it any more.
|
|
140
|
-
*/
|
|
141
|
-
export function presentedToken(authorization, target) {
|
|
142
|
-
if (typeof authorization === "string") {
|
|
143
|
-
const space = authorization.indexOf(" ");
|
|
144
|
-
if (space > 0 && authorization.slice(0, space).toLowerCase() === "bearer") {
|
|
145
|
-
const value = authorization.slice(space + 1).trim();
|
|
146
|
-
if (value !== "")
|
|
147
|
-
return value;
|
|
148
|
-
}
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
return target.searchParams.get(TOKEN_PARAMETER);
|
|
152
|
-
}
|
|
153
|
-
function constantTimeEquals(left, right) {
|
|
154
|
-
const a = Buffer.from(left, "utf8");
|
|
155
|
-
const b = Buffer.from(right, "utf8");
|
|
156
|
-
if (a.length !== b.length) {
|
|
157
|
-
/* Compare against itself so the work done does not leak the length. */
|
|
158
|
-
timingSafeEqual(a, a);
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
return timingSafeEqual(a, b);
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Build the document served on the quota route.
|
|
165
|
-
*
|
|
166
|
-
* The advice engine in the core does the work, exactly as it does for the
|
|
167
|
-
* statusline and the hook, so a phone can never see a number the agent would
|
|
168
|
-
* not have seen. A cache that cannot be read becomes an empty document with
|
|
169
|
-
* every provider unknown, never a zero and never a cap.
|
|
170
|
-
*/
|
|
171
|
-
export async function quotaDocument(stateDirectory, now) {
|
|
172
|
-
const cached = await readSnapshotCache(stateDirectory);
|
|
173
|
-
const advice = buildAdvice(cached.ok ? cached.snapshots : [], now, PROVIDER_CODES);
|
|
174
|
-
return {
|
|
175
|
-
schema: SERVE_SCHEMA,
|
|
176
|
-
generatedAt: now,
|
|
177
|
-
reason: advice.reason,
|
|
178
|
-
providers: advice.providers.map((entry) => ({
|
|
179
|
-
provider: entry.provider,
|
|
180
|
-
state: entry.state,
|
|
181
|
-
usagePercent: Number(floorFixed(entry.usagePercent, 2)),
|
|
182
|
-
resetAt: entry.resetAt
|
|
183
|
-
})),
|
|
184
|
-
unknown: [...advice.unknownProviders]
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
function securityHeaders() {
|
|
188
|
-
return {
|
|
189
|
-
"cache-control": "no-store",
|
|
190
|
-
"referrer-policy": "no-referrer",
|
|
191
|
-
"x-content-type-options": "nosniff",
|
|
192
|
-
"x-frame-options": "DENY",
|
|
193
|
-
/* No Access-Control-Allow-Origin is ever sent. The page is same origin. */
|
|
194
|
-
vary: "Origin"
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
function sendJson(request, response, status, body) {
|
|
198
|
-
const text = canonicalJson(body);
|
|
199
|
-
response.writeHead(status, {
|
|
200
|
-
...securityHeaders(),
|
|
201
|
-
"content-security-policy": "default-src 'none'",
|
|
202
|
-
"content-type": "application/json; charset=utf-8",
|
|
203
|
-
"content-length": String(Buffer.byteLength(text, "utf8"))
|
|
204
|
-
});
|
|
205
|
-
/* A HEAD response carries the headers of the GET and none of its body. */
|
|
206
|
-
response.end(request.method === "HEAD" ? undefined : text);
|
|
207
|
-
}
|
|
208
|
-
function sendHtml(request, response, status, html) {
|
|
209
|
-
response.writeHead(status, {
|
|
210
|
-
...securityHeaders(),
|
|
211
|
-
"content-security-policy": [
|
|
212
|
-
"default-src 'none'",
|
|
213
|
-
"style-src 'unsafe-inline'",
|
|
214
|
-
"script-src 'unsafe-inline'",
|
|
215
|
-
"connect-src 'self'",
|
|
216
|
-
"base-uri 'none'",
|
|
217
|
-
"form-action 'none'"
|
|
218
|
-
].join("; "),
|
|
219
|
-
"content-type": "text/html; charset=utf-8",
|
|
220
|
-
"content-length": String(Buffer.byteLength(html, "utf8"))
|
|
221
|
-
});
|
|
222
|
-
response.end(request.method === "HEAD" ? undefined : html);
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* The mobile page.
|
|
226
|
-
*
|
|
227
|
-
* One file, no build step, no external request of any kind. It reads the same
|
|
228
|
-
* JSON route a script would, on the same origin, carrying the same token that
|
|
229
|
-
* is already in the address bar. The colours mirror the design tokens on the
|
|
230
|
-
* site, and the whole thing follows the reader's own light or dark setting.
|
|
231
|
-
*/
|
|
232
|
-
export function mobilePage() {
|
|
233
|
-
return `<!doctype html>
|
|
234
|
-
<html lang="en">
|
|
235
|
-
<head>
|
|
236
|
-
<meta charset="utf-8">
|
|
237
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
238
|
-
<meta name="robots" content="noindex, nofollow">
|
|
239
|
-
<meta name="color-scheme" content="light dark">
|
|
240
|
-
<title>OpenLimiter quota</title>
|
|
241
|
-
<style>
|
|
242
|
-
:root{color-scheme:light dark;--canvas:#fff;--surface:#f6f6f7;--track:#e2e2e6;--hairline:#e5e5e8;--heading:#0d0d0f;--body:#53535c;--muted:#62626c;--accent:#0866ff;--warn:#b45309;--stop:#b42318}
|
|
243
|
-
@media (prefers-color-scheme:dark){:root{--canvas:#0a0a0b;--surface:#121214;--track:#26262b;--hairline:#232327;--heading:#f2f2f3;--body:#adadb6;--muted:#94949e;--accent:#6ba6ff;--warn:#f0b429;--stop:#ff6b6b}}
|
|
244
|
-
*{box-sizing:border-box}
|
|
245
|
-
body{margin:0;padding:20px 16px 48px;background:var(--canvas);color:var(--body);font:15px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;-webkit-font-smoothing:antialiased}
|
|
246
|
-
main{max-width:520px;margin:0 auto}
|
|
247
|
-
h1{margin:0 0 2px;font-size:19px;font-weight:600;letter-spacing:-.01em;color:var(--heading)}
|
|
248
|
-
.sub{margin:0 0 20px;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--muted)}
|
|
249
|
-
.state{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--hairline);border-radius:8px;padding:5px 10px;font:11px/1 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);background:var(--surface)}
|
|
250
|
-
.dot{width:7px;height:7px;border-radius:50%;background:var(--accent)}
|
|
251
|
-
.card{border:1px solid var(--hairline);border-radius:14px;background:var(--surface);padding:14px 16px;margin-top:10px}
|
|
252
|
-
.row{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
|
|
253
|
-
.name{font-size:14px;font-weight:600;color:var(--heading);letter-spacing:.01em}
|
|
254
|
-
.pct{font:600 20px/1 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--heading);font-variant-numeric:tabular-nums}
|
|
255
|
-
.bar{margin-top:10px;height:6px;border-radius:999px;background:var(--track);overflow:hidden}
|
|
256
|
-
.fill{height:100%;border-radius:999px;background:var(--accent);transition:width .3s ease}
|
|
257
|
-
.fill.near{background:var(--warn)}
|
|
258
|
-
.fill.at{background:var(--stop)}
|
|
259
|
-
.meta{margin-top:8px;display:flex;gap:10px;flex-wrap:wrap;font:11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--muted)}
|
|
260
|
-
.note{margin-top:22px;font-size:12px;line-height:1.6;color:var(--muted)}
|
|
261
|
-
.err{color:var(--stop)}
|
|
262
|
-
</style>
|
|
263
|
-
</head>
|
|
264
|
-
<body>
|
|
265
|
-
<main>
|
|
266
|
-
<h1>OpenLimiter</h1>
|
|
267
|
-
<p class="sub" id="stamp">Loading quota from this machine.</p>
|
|
268
|
-
<p><span class="state"><span class="dot" id="dot"></span><span id="reason">reading</span></span></p>
|
|
269
|
-
<div id="cards"></div>
|
|
270
|
-
<p class="note" id="unknown"></p>
|
|
271
|
-
<p class="note">
|
|
272
|
-
Served from the machine running OpenLimiter, over your own network. Nothing here
|
|
273
|
-
left that machine, nothing was uploaded, and this page can only read.
|
|
274
|
-
</p>
|
|
275
|
-
</main>
|
|
276
|
-
<script>
|
|
277
|
-
(function(){
|
|
278
|
-
/* The token arrives in the fragment, which no server ever sees. Move it into
|
|
279
|
-
this tab's own memory and wipe the address, so the history entry the phone
|
|
280
|
-
keeps, and syncs, carries no capability.
|
|
281
|
-
|
|
282
|
-
The query is read as well, and first, because an address printed by an
|
|
283
|
-
older build put the token there. Such an address still reaches this page,
|
|
284
|
-
and a page that only looked at the fragment would sit at 401 forever with
|
|
285
|
-
the token visible in the address bar the whole time. Reading it, then
|
|
286
|
-
scrubbing BOTH the query and the fragment in one replaceState, turns the
|
|
287
|
-
old address into the new behaviour on its first load. */
|
|
288
|
-
var key = ${JSON.stringify(TOKEN_STORAGE_KEY)};
|
|
289
|
-
var name = ${JSON.stringify(TOKEN_PARAMETER)};
|
|
290
|
-
var prefix = ${JSON.stringify(TOKEN_FRAGMENT_PREFIX)};
|
|
291
|
-
var token = "";
|
|
292
|
-
var fromAddress = false;
|
|
293
|
-
try {
|
|
294
|
-
var queried = new URLSearchParams(location.search).get(name);
|
|
295
|
-
if (queried) {
|
|
296
|
-
token = queried;
|
|
297
|
-
fromAddress = true;
|
|
298
|
-
} else if (location.hash.indexOf(prefix) === 0) {
|
|
299
|
-
token = decodeURIComponent(location.hash.slice(prefix.length));
|
|
300
|
-
fromAddress = true;
|
|
301
|
-
}
|
|
302
|
-
} catch (error) {
|
|
303
|
-
token = "";
|
|
304
|
-
}
|
|
305
|
-
if (fromAddress) {
|
|
306
|
-
try { sessionStorage.setItem(key, token); } catch (error) {}
|
|
307
|
-
/* One rewrite, dropping the query and the fragment together. */
|
|
308
|
-
try { history.replaceState(null, "", location.pathname); } catch (error) {}
|
|
309
|
-
} else {
|
|
310
|
-
try { token = sessionStorage.getItem(key) || ""; } catch (error) { token = ""; }
|
|
311
|
-
}
|
|
312
|
-
var cards = document.getElementById("cards");
|
|
313
|
-
var stamp = document.getElementById("stamp");
|
|
314
|
-
var reason = document.getElementById("reason");
|
|
315
|
-
var dot = document.getElementById("dot");
|
|
316
|
-
var unknown = document.getElementById("unknown");
|
|
317
|
-
function tone(value){ return value >= 100 ? "at" : value >= 80 ? "near" : ""; }
|
|
318
|
-
function clock(value){
|
|
319
|
-
if (value === null) return "no reset";
|
|
320
|
-
var when = new Date(value);
|
|
321
|
-
return isNaN(when.getTime()) ? "no reset" : "resets " + when.toLocaleString();
|
|
322
|
-
}
|
|
323
|
-
function draw(document_){
|
|
324
|
-
reason.textContent = document_.reason.toLowerCase().replace("_"," ");
|
|
325
|
-
dot.style.background = document_.reason === "AT_CAP" ? "var(--stop)"
|
|
326
|
-
: document_.reason === "NEAR_CAP" ? "var(--warn)" : "var(--accent)";
|
|
327
|
-
stamp.textContent = "Updated " + new Date(document_.generatedAt).toLocaleTimeString();
|
|
328
|
-
stamp.className = "sub";
|
|
329
|
-
cards.textContent = "";
|
|
330
|
-
document_.providers.forEach(function(entry){
|
|
331
|
-
var card = document.createElement("div");
|
|
332
|
-
card.className = "card";
|
|
333
|
-
var row = document.createElement("div");
|
|
334
|
-
row.className = "row";
|
|
335
|
-
var name = document.createElement("span");
|
|
336
|
-
name.className = "name";
|
|
337
|
-
name.textContent = entry.provider;
|
|
338
|
-
var pct = document.createElement("span");
|
|
339
|
-
pct.className = "pct";
|
|
340
|
-
pct.textContent = entry.usagePercent.toFixed(1) + "%";
|
|
341
|
-
row.appendChild(name); row.appendChild(pct);
|
|
342
|
-
var bar = document.createElement("div");
|
|
343
|
-
bar.className = "bar";
|
|
344
|
-
var fill = document.createElement("div");
|
|
345
|
-
fill.className = "fill " + tone(entry.usagePercent);
|
|
346
|
-
fill.style.width = Math.min(100, Math.max(0, entry.usagePercent)) + "%";
|
|
347
|
-
bar.appendChild(fill);
|
|
348
|
-
var meta = document.createElement("div");
|
|
349
|
-
meta.className = "meta";
|
|
350
|
-
var state = document.createElement("span");
|
|
351
|
-
state.textContent = entry.state;
|
|
352
|
-
var reset = document.createElement("span");
|
|
353
|
-
reset.textContent = clock(entry.resetAt);
|
|
354
|
-
meta.appendChild(state); meta.appendChild(reset);
|
|
355
|
-
card.appendChild(row); card.appendChild(bar); card.appendChild(meta);
|
|
356
|
-
cards.appendChild(card);
|
|
357
|
-
});
|
|
358
|
-
unknown.textContent = document_.unknown.length === 0
|
|
359
|
-
? ""
|
|
360
|
-
: "Unknown, so nothing is claimed for them: " + document_.unknown.join(", ") + ".";
|
|
361
|
-
}
|
|
362
|
-
function load(){
|
|
363
|
-
/* The token goes in a header, never in a URL, so nothing that repeats every
|
|
364
|
-
fifteen seconds can write it into a log or a history entry. */
|
|
365
|
-
fetch("/quota.json", {
|
|
366
|
-
cache: "no-store",
|
|
367
|
-
headers: { "authorization": "Bearer " + token }
|
|
368
|
-
})
|
|
369
|
-
.then(function(response){
|
|
370
|
-
if (!response.ok) throw new Error(String(response.status));
|
|
371
|
-
return response.json();
|
|
372
|
-
})
|
|
373
|
-
.then(draw)
|
|
374
|
-
.catch(function(error){
|
|
375
|
-
stamp.className = "sub err";
|
|
376
|
-
stamp.textContent = error.message === "401"
|
|
377
|
-
? "The token in this address is not valid. Scan the code again."
|
|
378
|
-
: "Cannot reach the machine running OpenLimiter.";
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
load();
|
|
382
|
-
setInterval(load, 15000);
|
|
383
|
-
})();
|
|
384
|
-
</script>
|
|
385
|
-
</body>
|
|
386
|
-
</html>
|
|
387
|
-
`;
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Start the local quota server.
|
|
391
|
-
*
|
|
392
|
-
* The returned handle carries the port that was actually bound, which matters
|
|
393
|
-
* when port zero was asked for, and closes every connection on shutdown so a
|
|
394
|
-
* caller is never left waiting on a keep alive socket.
|
|
395
|
-
*/
|
|
396
|
-
export async function startQuotaServer(options = {}) {
|
|
397
|
-
const host = options.host ?? DEFAULT_SERVE_HOST;
|
|
398
|
-
const port = options.port ?? DEFAULT_SERVE_PORT;
|
|
399
|
-
const token = options.token ?? randomBytes(TOKEN_BYTES).toString("base64url");
|
|
400
|
-
const clock = options.now ?? (() => new Date().toISOString());
|
|
401
|
-
const stateDirectory = options.stateDirectory;
|
|
402
|
-
/* Rewritten once the socket is bound, which is what port zero needs. */
|
|
403
|
-
let listeningPort = port;
|
|
404
|
-
async function handle(request, response) {
|
|
405
|
-
const method = request.method ?? "GET";
|
|
406
|
-
if (method !== "GET" && method !== "HEAD") {
|
|
407
|
-
response.writeHead(405, { ...securityHeaders(), allow: "GET, HEAD" });
|
|
408
|
-
response.end();
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
const rawTarget = request.url ?? "/";
|
|
412
|
-
if (rawTarget.length > MAX_TARGET_LENGTH) {
|
|
413
|
-
sendJson(request, response, 414, { error: "target_too_long" });
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
if (!isAllowedHostHeader(request.headers.host, listeningPort)) {
|
|
417
|
-
sendJson(request, response, 403, { error: "host_not_allowed" });
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
/* No cross origin caller is ever wanted, so any origin at all is refused. */
|
|
421
|
-
if (typeof request.headers.origin === "string") {
|
|
422
|
-
sendJson(request, response, 403, { error: "cross_origin_refused" });
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
let target;
|
|
426
|
-
try {
|
|
427
|
-
target = new URL(rawTarget, "http://localhost");
|
|
428
|
-
}
|
|
429
|
-
catch {
|
|
430
|
-
sendJson(request, response, 400, { error: "bad_request" });
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
/*
|
|
434
|
-
* The shell is served before the token is looked at, and only the shell.
|
|
435
|
-
* It has to be, because the token is in the fragment and the fragment never
|
|
436
|
-
* arrives here. It carries no reading, no token and no account detail, so
|
|
437
|
-
* anyone who fetches it learns only that this command is running, which the
|
|
438
|
-
* open port already told them.
|
|
439
|
-
*/
|
|
440
|
-
if (target.pathname === "/") {
|
|
441
|
-
sendHtml(request, response, 200, mobilePage());
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
const presented = presentedToken(request.headers.authorization, target);
|
|
445
|
-
if (presented === null || !constantTimeEquals(presented, token)) {
|
|
446
|
-
sendJson(request, response, 401, { error: "token_required" });
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
if (target.pathname === "/quota.json") {
|
|
450
|
-
sendJson(request, response, 200, await quotaDocument(stateDirectory, clock()));
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
sendJson(request, response, 404, { error: "not_found" });
|
|
454
|
-
}
|
|
455
|
-
const server = createServer((request, response) => {
|
|
456
|
-
/* Nothing here consumes a body. Draining keeps the socket reusable. */
|
|
457
|
-
request.resume();
|
|
458
|
-
handle(request, response).catch(() => {
|
|
459
|
-
if (!response.headersSent) {
|
|
460
|
-
sendJson(request, response, 500, { error: "internal" });
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
response.end();
|
|
464
|
-
});
|
|
465
|
-
});
|
|
466
|
-
server.headersTimeout = HEADERS_TIMEOUT_MILLISECONDS;
|
|
467
|
-
server.requestTimeout = REQUEST_TIMEOUT_MILLISECONDS;
|
|
468
|
-
server.keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MILLISECONDS;
|
|
469
|
-
server.maxConnections = MAX_CONNECTIONS;
|
|
470
|
-
await new Promise((resolve, reject) => {
|
|
471
|
-
const onError = (error) => {
|
|
472
|
-
server.off("listening", onListening);
|
|
473
|
-
reject(error instanceof Error ? error : new Error("listen failed"));
|
|
474
|
-
};
|
|
475
|
-
const onListening = () => {
|
|
476
|
-
server.off("error", onError);
|
|
477
|
-
const address = server.address();
|
|
478
|
-
if (address !== null && typeof address === "object") {
|
|
479
|
-
listeningPort = address.port;
|
|
480
|
-
}
|
|
481
|
-
resolve();
|
|
482
|
-
};
|
|
483
|
-
server.once("error", onError);
|
|
484
|
-
server.once("listening", onListening);
|
|
485
|
-
server.listen(port, host);
|
|
486
|
-
});
|
|
487
|
-
/* Fragment, not query. See TOKEN_FRAGMENT_PREFIX above for why. */
|
|
488
|
-
const suffix = "/" + TOKEN_FRAGMENT_PREFIX + token;
|
|
489
|
-
const advertised = host === "0.0.0.0" || host === "::"
|
|
490
|
-
? lanAddress() ?? "127.0.0.1"
|
|
491
|
-
: host;
|
|
492
|
-
return {
|
|
493
|
-
host,
|
|
494
|
-
port: listeningPort,
|
|
495
|
-
token,
|
|
496
|
-
url: "http://" + authority(advertised, listeningPort) + suffix,
|
|
497
|
-
localUrl: "http://" + authority("127.0.0.1", listeningPort) + suffix,
|
|
498
|
-
close: async () => {
|
|
499
|
-
await new Promise((resolve) => {
|
|
500
|
-
server.close(() => {
|
|
501
|
-
resolve();
|
|
502
|
-
});
|
|
503
|
-
server.closeAllConnections();
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* The text printed when the server starts.
|
|
510
|
-
*
|
|
511
|
-
* The address is printed before the symbol and again after it, because a
|
|
512
|
-
* terminal that cannot draw the symbol still has to be usable, and because the
|
|
513
|
-
* address is the thing a person types when a camera will not focus.
|
|
514
|
-
*/
|
|
515
|
-
export function serveBanner(handle, options = {}) {
|
|
516
|
-
const lines = [
|
|
517
|
-
"OpenLimiter is serving read only quota on your local network.",
|
|
518
|
-
"",
|
|
519
|
-
" " + handle.url,
|
|
520
|
-
""
|
|
521
|
-
];
|
|
522
|
-
if (options.withoutQr !== true) {
|
|
523
|
-
try {
|
|
524
|
-
lines.push(renderQr(encodeQr(handle.url), { color: options.color ?? false }), "");
|
|
525
|
-
}
|
|
526
|
-
catch {
|
|
527
|
-
lines.push("The address is too long to draw as a scannable code.", "");
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
lines.push("On this machine: " + handle.localUrl, "", "Scan the code with a phone on the same network, or type the address.", "The token changes every time this command starts, and nothing answers without it.", "The code and the address ARE the key: anyone who can see this screen, or a", "photograph of it, can read your quota for as long as this command runs.", "Read only: no route here can change anything on this machine, and no", "credential, provider payload, or account detail is ever served.", "Meant for a trusted home or office network. This listens on every interface", "on this machine, so do not port forward this port and do not open it on a", "firewall: nothing but your router's default is keeping it off the internet.", "", "Press Ctrl C to stop.");
|
|
531
|
-
return lines.join("\n");
|
|
532
|
-
}
|
|
533
|
-
//# sourceMappingURL=serve.js.map
|
package/dist/serve.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"serve.js","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"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"}
|