@redirhub/cloudflare-redirects 0.1.1 → 0.2.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 +10 -6
- package/dist/core/redirects.d.ts +10 -0
- package/dist/core/redirects.js +3 -2
- package/dist/core/report.d.ts +7 -4
- package/dist/core/report.js +57 -15
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,10 +99,12 @@ add your own logic, pass `origin: (request, env, ctx) => …`.
|
|
|
99
99
|
| Option | |
|
|
100
100
|
| --- | --- |
|
|
101
101
|
| `settings(env)` | The rules address and the key from the Worker's bindings. Empty address: off, every request goes to the origin. |
|
|
102
|
+
| `report` | `false`, or `(request) => boolean`: whether to report, over the key. Default: whenever there is a key. |
|
|
102
103
|
| `origin(request, env, ctx)` | Where requests no rule answers go; default `fetch(request)`. |
|
|
103
104
|
| `report404` | `rules` (default): 404s of hostnames with **Report 404 pages** on; `all`: every hostname the Worker serves. |
|
|
104
105
|
| `hostname(request)` | The hostname to look up in the rules; `null` leaves the request alone (straight to the origin, nothing reported). Default the `Host` header. |
|
|
105
|
-
| `path(request)` | The path
|
|
106
|
+
| `path(request)` | The path to look up in the rules, percent-encoded; `null` leaves the request alone. Default the request URL's path. |
|
|
107
|
+
| `location(location, request)` | The `Location` to send, from the one the rule gives; also the reported destination. |
|
|
106
108
|
| `clientIp(request)` | The visitor's address in reports; default `CF-Connecting-IP`. `null` writes `-`. |
|
|
107
109
|
| `userAgent(request)` | The User-Agent in reports; default the `User-Agent` header. `null` writes `-`. |
|
|
108
110
|
| `anonymizeIp` | Report IPv4 with the last byte zeroed, IPv6 cut to /48. |
|
|
@@ -127,7 +129,8 @@ export default createWorker<Env>({
|
|
|
127
129
|
const { pathname } = new URL(request.url)
|
|
128
130
|
return pathname.startsWith('/api/') ? null : pathname
|
|
129
131
|
},
|
|
130
|
-
|
|
132
|
+
// Testing on workers.dev redirects, but is not reported.
|
|
133
|
+
report: (request) => !new URL(request.url).hostname.endsWith('.workers.dev'),
|
|
131
134
|
anonymizeIp: true,
|
|
132
135
|
})
|
|
133
136
|
```
|
|
@@ -165,10 +168,11 @@ ignored: a Worker keeps no connection open.
|
|
|
165
168
|
|
|
166
169
|
**Reports.** A line queued while nothing is being sent goes out at once; lines queued while a
|
|
167
170
|
request is out go together in the next one (at most 100 lines or 64 KB, gzip with
|
|
168
|
-
`CompressionStream`, 2 second timeout)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
lines are dropped.
|
|
171
|
+
`CompressionStream`, 2 second timeout), and each full batch opens another request, up to four at
|
|
172
|
+
once. Only the request that starts sending waits for them, in `ctx.waitUntil`, for at most 10
|
|
173
|
+
seconds; the others only add their line, so no request waits on another's work. A failed or
|
|
174
|
+
rejected batch is dropped, never retried; beyond 10,000 waiting lines, lines are dropped. Dropped
|
|
175
|
+
lines are logged at once, then at most every ten minutes with the total by reason.
|
|
172
176
|
|
|
173
177
|
## Privacy
|
|
174
178
|
|
package/dist/core/redirects.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface RedirectsOptions {
|
|
|
13
13
|
rulesUrl: string | null | undefined;
|
|
14
14
|
/** The integration key. Without it redirects work and nothing is reported. */
|
|
15
15
|
key?: string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Whether to report, over the key: `false` reports nothing, a function decides per request
|
|
18
|
+
* (e.g. not for preview deployments). Default: report whenever there is a key.
|
|
19
|
+
*/
|
|
20
|
+
report?: boolean | ((request: Request) => boolean);
|
|
16
21
|
/**
|
|
17
22
|
* Which 404 answers of the site are reported (where the site's answer is seen, `afterSite`):
|
|
18
23
|
* `rules` (default) for hostnames whose `@host` line has `report404`, `all` for every hostname.
|
|
@@ -32,6 +37,11 @@ export interface RedirectsOptions {
|
|
|
32
37
|
* your code sees it, e.g. a proxy that strips a prefix.
|
|
33
38
|
*/
|
|
34
39
|
path?: (request: Request) => string | null;
|
|
40
|
+
/**
|
|
41
|
+
* The `Location` to send, from the one the rule gives (e.g. a same-site target in the visitor's
|
|
42
|
+
* language). It is also what the report names as the destination. Default: as the rule gives it.
|
|
43
|
+
*/
|
|
44
|
+
location?: (location: string, request: Request) => string;
|
|
35
45
|
/**
|
|
36
46
|
* The visitor's address in reports: only a value the platform sets, never a header a client can
|
|
37
47
|
* forge. Null writes `-`.
|
package/dist/core/redirects.js
CHANGED
|
@@ -43,7 +43,7 @@ export function createRedirects(options) {
|
|
|
43
43
|
};
|
|
44
44
|
const keepAliveOf = (context) => (context ? (work) => context.waitUntil(work) : undefined);
|
|
45
45
|
const report = (request, url, host, status, size, keepAlive, redirect) => {
|
|
46
|
-
if (!reporter) {
|
|
46
|
+
if (!reporter || options.report === false || (typeof options.report === 'function' && !options.report(request))) {
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
const ip = options.clientIp?.(request) ?? null;
|
|
@@ -61,7 +61,8 @@ export function createRedirects(options) {
|
|
|
61
61
|
}, now()), keepAlive);
|
|
62
62
|
};
|
|
63
63
|
const redirect = (request, url, rules, host, rule, keepAlive) => {
|
|
64
|
-
const
|
|
64
|
+
const target = buildLocation(rule, url);
|
|
65
|
+
const location = options.location ? options.location(target, request) : target;
|
|
65
66
|
const headers = new Headers({ Location: byteString(location) });
|
|
66
67
|
if (rules.poweredBy) {
|
|
67
68
|
headers.set('X-Powered-By', rules.poweredBy);
|
package/dist/core/report.d.ts
CHANGED
|
@@ -47,15 +47,18 @@ export interface ReporterOptions {
|
|
|
47
47
|
maxQueue?: number;
|
|
48
48
|
/** How long one invocation keeps sending before it leaves the rest to the next; default 10,000. */
|
|
49
49
|
budgetMs?: number;
|
|
50
|
+
/** Requests out at the same time; default 4. */
|
|
51
|
+
maxInFlight?: number;
|
|
50
52
|
}
|
|
51
53
|
/**
|
|
52
54
|
* Sends report lines without delaying a response and without a timer.
|
|
53
55
|
*
|
|
54
56
|
* A line queued while nothing is being sent goes out at once, so a site with little traffic
|
|
55
|
-
* reports each visit immediately and keeps nothing waiting. Lines queued while
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
57
|
+
* reports each visit immediately and keeps nothing waiting. Lines queued while requests are out
|
|
58
|
+
* go together in the next one (at most 100 lines, 64 KB, gzip), so a burst costs one request per
|
|
59
|
+
* hundred visits; up to `maxInFlight` (4) requests are out at once, so a busy instance keeps up.
|
|
60
|
+
* The invocation that starts sending keeps itself alive (through `keepAlive`) until the queue is
|
|
61
|
+
* empty and its requests are back; the others only add their line, so no request waits on
|
|
59
62
|
* another request's work. A failed or rejected batch is dropped and counted, never retried.
|
|
60
63
|
*/
|
|
61
64
|
export declare class Reporter {
|
package/dist/core/report.js
CHANGED
|
@@ -73,17 +73,25 @@ const DROP_LOG_EVERY_MS = 10 * 60_000;
|
|
|
73
73
|
* Sends report lines without delaying a response and without a timer.
|
|
74
74
|
*
|
|
75
75
|
* A line queued while nothing is being sent goes out at once, so a site with little traffic
|
|
76
|
-
* reports each visit immediately and keeps nothing waiting. Lines queued while
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
76
|
+
* reports each visit immediately and keeps nothing waiting. Lines queued while requests are out
|
|
77
|
+
* go together in the next one (at most 100 lines, 64 KB, gzip), so a burst costs one request per
|
|
78
|
+
* hundred visits; up to `maxInFlight` (4) requests are out at once, so a busy instance keeps up.
|
|
79
|
+
* The invocation that starts sending keeps itself alive (through `keepAlive`) until the queue is
|
|
80
|
+
* empty and its requests are back; the others only add their line, so no request waits on
|
|
80
81
|
* another request's work. A failed or rejected batch is dropped and counted, never retried.
|
|
81
82
|
*/
|
|
82
83
|
export class Reporter {
|
|
83
84
|
#options;
|
|
84
85
|
#queue = [];
|
|
86
|
+
/** The sender: the invocation that started it keeps it alive until it is done. */
|
|
85
87
|
#sending = null;
|
|
86
|
-
#
|
|
88
|
+
/** The sender's requests out now, and when it started (for the budget); #running once it has. */
|
|
89
|
+
#out = new Set();
|
|
90
|
+
#started = 0;
|
|
91
|
+
#running = false;
|
|
92
|
+
/** Lines dropped since the last log, by reason. */
|
|
93
|
+
#dropped = new Map();
|
|
94
|
+
#droppedSince = 0;
|
|
87
95
|
#lastDropLog = Number.NEGATIVE_INFINITY;
|
|
88
96
|
constructor(options) {
|
|
89
97
|
// `??` per field, not a spread: an option passed as undefined keeps its default.
|
|
@@ -95,6 +103,7 @@ export class Reporter {
|
|
|
95
103
|
timeoutMs: options.timeoutMs ?? 2000,
|
|
96
104
|
maxQueue: options.maxQueue ?? 10_000,
|
|
97
105
|
budgetMs: options.budgetMs ?? 10_000,
|
|
106
|
+
maxInFlight: options.maxInFlight ?? 4,
|
|
98
107
|
};
|
|
99
108
|
}
|
|
100
109
|
/** Queues one line; `keepAlive` holds the invocation open while this call has to send. */
|
|
@@ -102,6 +111,7 @@ export class Reporter {
|
|
|
102
111
|
if (this.#options.key === '') {
|
|
103
112
|
return;
|
|
104
113
|
}
|
|
114
|
+
this.#logDrops();
|
|
105
115
|
if (this.#queue.length >= this.#options.maxQueue) {
|
|
106
116
|
this.#drop(1, 'queue full');
|
|
107
117
|
return;
|
|
@@ -111,6 +121,9 @@ export class Reporter {
|
|
|
111
121
|
this.#sending = this.#send();
|
|
112
122
|
keepAlive?.(this.#sending);
|
|
113
123
|
}
|
|
124
|
+
else if (this.#running) {
|
|
125
|
+
this.#fill();
|
|
126
|
+
}
|
|
114
127
|
}
|
|
115
128
|
/** Resolves once everything queued so far has been sent or dropped. */
|
|
116
129
|
async flush() {
|
|
@@ -125,14 +138,31 @@ export class Reporter {
|
|
|
125
138
|
async #send() {
|
|
126
139
|
// Lines queued in the same turn of the event loop go in the first request.
|
|
127
140
|
await Promise.resolve();
|
|
128
|
-
|
|
141
|
+
this.#started = Date.now();
|
|
142
|
+
this.#running = true;
|
|
129
143
|
for (;;) {
|
|
130
|
-
|
|
144
|
+
this.#fill();
|
|
145
|
+
if (this.#out.size === 0) {
|
|
131
146
|
// Checked and cleared in one step: a line queued after this starts a new sender.
|
|
147
|
+
this.#running = false;
|
|
132
148
|
this.#sending = null;
|
|
133
149
|
return;
|
|
134
150
|
}
|
|
135
|
-
await
|
|
151
|
+
await Promise.race(this.#out);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Starts requests for the waiting lines, within the budget: one when none is out, and more,
|
|
156
|
+
* up to maxInFlight, only for full batches, so lines that arrive while a request is out wait
|
|
157
|
+
* for it and go together. Called by the sender and on each line queued while it runs.
|
|
158
|
+
*/
|
|
159
|
+
#fill() {
|
|
160
|
+
while (this.#out.size < this.#options.maxInFlight &&
|
|
161
|
+
this.#queue.length > 0 &&
|
|
162
|
+
(this.#out.size === 0 || this.#queue.length >= MAX_BATCH_LINES) &&
|
|
163
|
+
Date.now() - this.#started < this.#options.budgetMs) {
|
|
164
|
+
const post = this.#post(this.#take()).finally(() => this.#out.delete(post));
|
|
165
|
+
this.#out.add(post);
|
|
136
166
|
}
|
|
137
167
|
}
|
|
138
168
|
#take() {
|
|
@@ -180,15 +210,27 @@ export class Reporter {
|
|
|
180
210
|
this.#drop(lines.length, error instanceof Error ? error.name : 'error');
|
|
181
211
|
}
|
|
182
212
|
}
|
|
183
|
-
/** Counts dropped lines
|
|
213
|
+
/** Counts dropped lines by reason; see #logDrops. */
|
|
184
214
|
#drop(lines, reason) {
|
|
185
|
-
this.#dropped
|
|
186
|
-
|
|
187
|
-
|
|
215
|
+
if (this.#dropped.size === 0) {
|
|
216
|
+
this.#droppedSince = Date.now();
|
|
217
|
+
}
|
|
218
|
+
this.#dropped.set(reason, (this.#dropped.get(reason) ?? 0) + lines);
|
|
219
|
+
this.#logDrops();
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Logs the dropped lines: the first drop at once, then at most once per ten minutes with the
|
|
223
|
+
* total since the last log, by reason. Called on every drop and every line queued, so a count
|
|
224
|
+
* is logged even after the drops stop, without a timer.
|
|
225
|
+
*/
|
|
226
|
+
#logDrops() {
|
|
227
|
+
if (this.#dropped.size === 0 || Date.now() - this.#lastDropLog < DROP_LOG_EVERY_MS) {
|
|
188
228
|
return;
|
|
189
229
|
}
|
|
190
|
-
this.#
|
|
191
|
-
this.#dropped
|
|
192
|
-
this.#
|
|
230
|
+
const total = [...this.#dropped.values()].reduce((sum, n) => sum + n, 0);
|
|
231
|
+
const reasons = [...this.#dropped].map(([reason, n]) => `${reason} ${n}`).join(', ');
|
|
232
|
+
this.#options.logger?.warn(`${PACKAGE}: ${total} report lines dropped since ${new Date(this.#droppedSince).toISOString()} (${reasons})`);
|
|
233
|
+
this.#dropped.clear();
|
|
234
|
+
this.#lastDropLog = Date.now();
|
|
193
235
|
}
|
|
194
236
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** This package: names its log lines and the User-Agent of rules downloads and reports. */
|
|
2
2
|
export declare const PACKAGE = "cloudflare-redirects";
|
|
3
3
|
/** Equal to package.json's version (a test checks it). */
|
|
4
|
-
export declare const VERSION = "0.
|
|
5
|
-
export declare const USER_AGENT = "cloudflare-redirects/0.
|
|
4
|
+
export declare const VERSION = "0.2.1";
|
|
5
|
+
export declare const USER_AGENT = "cloudflare-redirects/0.2.1";
|
package/dist/version.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** This package: names its log lines and the User-Agent of rules downloads and reports. */
|
|
2
2
|
export const PACKAGE = 'cloudflare-redirects';
|
|
3
3
|
/** Equal to package.json's version (a test checks it). */
|
|
4
|
-
export const VERSION = '0.
|
|
4
|
+
export const VERSION = '0.2.1';
|
|
5
5
|
export const USER_AGENT = `${PACKAGE}/${VERSION}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redirhub/cloudflare-redirects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Bulk redirects and 404 monitoring for Cloudflare Workers from a remote rules file: thousands of 301/302 redirects, short links and website migrations in front of your origin, live within a minute without a redeploy. RedirHub Integration Mode.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|