@redirhub/cloudflare-redirects 0.2.1 → 0.3.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/README.md +19 -7
- package/dist/core/redirects.d.ts +2 -1
- package/dist/core/redirects.js +14 -4
- package/dist/core/report.d.ts +4 -4
- package/dist/core/report.js +2 -2
- package/dist/core/rules.d.ts +15 -5
- package/dist/core/rules.js +33 -4
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,8 +9,8 @@ in front of your origin, from a remote rules file, and monitor its 404s.** Old U
|
|
|
9
9
|
whole website migrations redirect at Cloudflare's edge; every redirect and every 404 is reported
|
|
10
10
|
back, so broken links show up before your visitors find them.
|
|
11
11
|
|
|
12
|
-
- Redirects, short links and whole website migrations, live within a minute,
|
|
13
|
-
redeploying the Worker.
|
|
12
|
+
- Redirects, short links, frame pages and whole website migrations, live within a minute,
|
|
13
|
+
without redeploying the Worker.
|
|
14
14
|
- 404 monitoring: the origin's 404s that no rule redirected are reported in the background.
|
|
15
15
|
- Fails open: no rules yet, a download error or a bug never breaks a page; the request goes on to
|
|
16
16
|
the origin.
|
|
@@ -148,7 +148,7 @@ the origin unchanged.
|
|
|
148
148
|
1. **Check path** (`/_redirhub/check`): `200`, `text/plain`, `Cache-Control: no-store`, body
|
|
149
149
|
`token=<check token> etag=<ETag of the rules in use>`. It proves the hostname is yours and
|
|
150
150
|
tells RedirHub which rules you serve.
|
|
151
|
-
2. **Exact rules** redirect before the origin, even when the page exists.
|
|
151
|
+
2. **Exact rules** redirect (or show their frame page) before the origin, even when the page exists.
|
|
152
152
|
3. Otherwise the origin answers. Only when it answers **404** and the hostname has a **catch-all
|
|
153
153
|
rule** (`{host}/*`), the 404 is discarded and the Worker redirects instead; a working page is
|
|
154
154
|
never taken over. Without a catch-all, the 404 goes to the visitor and is reported when the
|
|
@@ -158,13 +158,24 @@ Matching follows format v1: the hostname in any case, port ignored; the path cas
|
|
|
158
158
|
percent-decoded, leading and trailing slashes ignored; scheme and query ignored. Every redirect
|
|
159
159
|
carries `X-Powered-By` when the rules file has `@powered-by`.
|
|
160
160
|
|
|
161
|
+
**Frame pages.** A rule with status `frame` shows its target in a frame instead of redirecting:
|
|
162
|
+
`200`, `text/html; charset=utf-8`, a page whose one full-window `<iframe>` loads the address a
|
|
163
|
+
redirect would send (flags `p` and `q` applied), titled with the target's hostname; the visitor's
|
|
164
|
+
address bar keeps their URL. It is matched like a redirect (an exact rule before the origin, a
|
|
165
|
+
catch-all only on the origin's 404), carries `X-Powered-By` and is reported, with status `200`.
|
|
166
|
+
HEAD gets the same status and headers without the page.
|
|
167
|
+
|
|
168
|
+
**Options** in a rule's fifth column (`password=`, `destinations=`, `routing=`, `plugins=`, and any
|
|
169
|
+
other) are ignored, as the format allows: the rule redirects, or shows its frame, to its target.
|
|
170
|
+
|
|
161
171
|
**Rules download.** The first request of a new isolate starts the download and waits for it at
|
|
162
172
|
most one second; if the rules server is slower, that request goes to the origin without rules and
|
|
163
173
|
the download (5 second timeout) finishes in `ctx.waitUntil`. Requests that arrive meanwhile do not
|
|
164
174
|
wait. After that the copy in memory is used, and once it is older than `@refresh` seconds (60 by
|
|
165
175
|
default) it is revalidated in `ctx.waitUntil` with `If-None-Match`, so no visitor waits. Any
|
|
166
176
|
failure keeps the last good copy and waits 30 seconds before trying again. `@notify` (push) is
|
|
167
|
-
ignored: a Worker keeps no connection open.
|
|
177
|
+
ignored: a Worker keeps no connection open. Rules downloads and reports identify themselves as
|
|
178
|
+
`User-Agent: cloudflare-redirects/<version>`.
|
|
168
179
|
|
|
169
180
|
**Reports.** A line queued while nothing is being sent goes out at once; lines queued while a
|
|
170
181
|
request is out go together in the next one (at most 100 lines or 64 KB, gzip with
|
|
@@ -185,9 +196,10 @@ Each report is one Apache combined line with the absolute URL:
|
|
|
185
196
|
That is the visitor's IP address (`CF-Connecting-IP`, or anonymized with `anonymizeIp`), the time
|
|
186
197
|
(UTC), the method, the requested URL with its query string, the status, the response size, the
|
|
187
198
|
Referer and the User-Agent (each cut to 1 KB); for a redirect also the rule's source and the
|
|
188
|
-
`Location` sent
|
|
189
|
-
|
|
190
|
-
|
|
199
|
+
`Location` sent, for a frame page the rule's source and the frame's address. The protocol is
|
|
200
|
+
always written `HTTP/1.1`. No cookies, other headers or bodies are sent. Only redirects, frame
|
|
201
|
+
pages, and 404s of hostnames with 404 reporting on, are reported; all other traffic stays between
|
|
202
|
+
the visitor and your origin. Reports go only to the `@hits` address of your
|
|
191
203
|
rules file, with your key.
|
|
192
204
|
|
|
193
205
|
## Limits
|
package/dist/core/redirects.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The integration on any runtime with the Fetch API (Request, Response, fetch): answers the check
|
|
3
3
|
* path, redirects exact rules before the site, redirects a hostname's catch-all where the site
|
|
4
|
-
* answers 404,
|
|
4
|
+
* answers 404, shows `frame` rules in a frame page instead of redirecting, and reports redirects,
|
|
5
|
+
* frame pages and 404s. The platform entry (`src/index.ts`) only says
|
|
5
6
|
* where the visitor's address and the background work come from.
|
|
6
7
|
*
|
|
7
8
|
* `src/core/` is the same in every JavaScript implementation (nextjs-redirects,
|
package/dist/core/redirects.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The integration on any runtime with the Fetch API (Request, Response, fetch): answers the check
|
|
3
3
|
* path, redirects exact rules before the site, redirects a hostname's catch-all where the site
|
|
4
|
-
* answers 404,
|
|
4
|
+
* answers 404, shows `frame` rules in a frame page instead of redirecting, and reports redirects,
|
|
5
|
+
* frame pages and 404s. The platform entry (`src/index.ts`) only says
|
|
5
6
|
* where the visitor's address and the background work come from.
|
|
6
7
|
*
|
|
7
8
|
* `src/core/` is the same in every JavaScript implementation (nextjs-redirects,
|
|
8
9
|
* cloudflare-redirects); CI compares it with the others.
|
|
9
10
|
*/
|
|
10
11
|
import { hitLine, Reporter } from "./report.js";
|
|
11
|
-
import { buildLocation, matchExact, normalizeHost } from "./rules.js";
|
|
12
|
+
import { buildLocation, framePage, matchExact, normalizeHost } from "./rules.js";
|
|
12
13
|
import { RulesSource } from "./source.js";
|
|
13
14
|
import { PACKAGE } from "../version.js";
|
|
14
15
|
const WARN_EVERY_MS = 10 * 60_000;
|
|
@@ -60,14 +61,23 @@ export function createRedirects(options) {
|
|
|
60
61
|
redirect,
|
|
61
62
|
}, now()), keepAlive);
|
|
62
63
|
};
|
|
64
|
+
/** A rule's answer: its redirect, or for a `frame` rule a `200` page framing the same address. */
|
|
63
65
|
const redirect = (request, url, rules, host, rule, keepAlive) => {
|
|
64
66
|
const target = buildLocation(rule, url);
|
|
65
67
|
const location = options.location ? options.location(target, request) : target;
|
|
66
|
-
const headers = new Headers(
|
|
68
|
+
const headers = new Headers();
|
|
67
69
|
if (rules.poweredBy) {
|
|
68
70
|
headers.set('X-Powered-By', rules.poweredBy);
|
|
69
71
|
}
|
|
70
|
-
|
|
72
|
+
// A frame page is reported like a redirect, with status 200 and size 0 as the format says.
|
|
73
|
+
report(request, url, host.name, rule.status === 'frame' ? 200 : rule.status, 0, keepAlive, { source: rule.source, location });
|
|
74
|
+
if (rule.status === 'frame') {
|
|
75
|
+
const page = framePage(location, rule.target);
|
|
76
|
+
headers.set('Content-Type', 'text/html; charset=utf-8');
|
|
77
|
+
headers.set('Content-Length', String(new TextEncoder().encode(page).length));
|
|
78
|
+
return new Response(request.method === 'HEAD' ? null : page, { status: 200, headers });
|
|
79
|
+
}
|
|
80
|
+
headers.set('Location', byteString(location));
|
|
71
81
|
return new Response(null, { status: rule.status, headers });
|
|
72
82
|
};
|
|
73
83
|
const handle = async (request, context) => {
|
package/dist/core/report.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Visit reports to the rules file's `@hits` address: one Apache combined line per redirect
|
|
3
|
-
* 404, as specified in redirhub/integration-spec (spec/v1/reports.txt is the golden sample).
|
|
2
|
+
* Visit reports to the rules file's `@hits` address: one Apache combined line per redirect, frame
|
|
3
|
+
* page or 404, as specified in redirhub/integration-spec (spec/v1/reports.txt is the golden sample).
|
|
4
4
|
*/
|
|
5
5
|
import type { Logger } from './log.ts';
|
|
6
6
|
export interface Hit {
|
|
@@ -14,11 +14,11 @@ export interface Hit {
|
|
|
14
14
|
/** The request URL; its escaped path and query are the request target. */
|
|
15
15
|
url: URL;
|
|
16
16
|
status: number;
|
|
17
|
-
/** Body bytes sent: 0 for a redirect. */
|
|
17
|
+
/** Body bytes sent: 0 for a redirect and a frame page. */
|
|
18
18
|
size: number;
|
|
19
19
|
referer: string | null;
|
|
20
20
|
userAgent: string | null;
|
|
21
|
-
/** For a redirect: the rule's source column and the `Location` sent
|
|
21
|
+
/** For a redirect or frame page: the rule's source column and the `Location` sent, or the frame's `src`. */
|
|
22
22
|
redirect?: {
|
|
23
23
|
source: string;
|
|
24
24
|
location: string;
|
package/dist/core/report.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Visit reports to the rules file's `@hits` address: one Apache combined line per redirect
|
|
3
|
-
* 404, as specified in redirhub/integration-spec (spec/v1/reports.txt is the golden sample).
|
|
2
|
+
* Visit reports to the rules file's `@hits` address: one Apache combined line per redirect, frame
|
|
3
|
+
* page or 404, as specified in redirhub/integration-spec (spec/v1/reports.txt is the golden sample).
|
|
4
4
|
*/
|
|
5
5
|
import { PACKAGE, USER_AGENT } from "../version.js";
|
|
6
6
|
/** The Referer and User-Agent are cut to this many bytes. */
|
package/dist/core/rules.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export declare const DEFAULT_REFRESH = 60;
|
|
|
12
12
|
export declare const MIN_REFRESH = 30;
|
|
13
13
|
export declare const MAX_REFRESH = 3600;
|
|
14
14
|
export type RedirectStatus = 301 | 302 | 307 | 308;
|
|
15
|
+
/** A rule's status column: a redirect status, or `frame` (a page that shows the target in a frame). */
|
|
16
|
+
export type RuleStatus = RedirectStatus | 'frame';
|
|
15
17
|
/** A rules file whose `@format` major version is not 1: the caller keeps its last good copy. */
|
|
16
18
|
export declare class UnknownFormatError extends Error {
|
|
17
19
|
name: string;
|
|
@@ -20,7 +22,7 @@ export interface Rule {
|
|
|
20
22
|
/** The source column as written; reports name the rule with it (`file=`). */
|
|
21
23
|
source: string;
|
|
22
24
|
target: string;
|
|
23
|
-
status:
|
|
25
|
+
status: RuleStatus;
|
|
24
26
|
/** `p`: append the request path (catch-all only). */
|
|
25
27
|
appendPath: boolean;
|
|
26
28
|
/** `q`: keep the request query string. */
|
|
@@ -46,7 +48,7 @@ export interface RuleSet {
|
|
|
46
48
|
/** Where redirects and 404s are reported; null disables reporting. */
|
|
47
49
|
hitsUrl: string | null;
|
|
48
50
|
checkPath: string;
|
|
49
|
-
/** Sent as `X-Powered-By` on every redirect from the rules; null sends none. */
|
|
51
|
+
/** Sent as `X-Powered-By` on every redirect and frame page from the rules; null sends none. */
|
|
50
52
|
poweredBy: string | null;
|
|
51
53
|
/** The `@notify` push channel. Parsed for completeness; this package polls. */
|
|
52
54
|
notifyUrl: string | null;
|
|
@@ -75,8 +77,16 @@ export declare function cleanEtag(etag: string): string;
|
|
|
75
77
|
/** The exact rule for a request URL on a listed hostname, or null. */
|
|
76
78
|
export declare function matchExact(host: HostRules, url: URL): Rule | null;
|
|
77
79
|
/**
|
|
78
|
-
* The `Location` of a rule for a request URL
|
|
79
|
-
* appended to the target's path without a double slash; with
|
|
80
|
-
* added with `?` or `&`, otherwise dropped. A fragment of the
|
|
80
|
+
* The `Location` of a rule for a request URL, and a `frame` rule's iframe `src`. With `appendPath`
|
|
81
|
+
* the escaped request path is appended to the target's path without a double slash; with
|
|
82
|
+
* `keepQuery` the request query is added with `?` or `&`, otherwise dropped. A fragment of the
|
|
83
|
+
* target stays last.
|
|
81
84
|
*/
|
|
82
85
|
export declare function buildLocation(rule: Rule, url: URL): string;
|
|
86
|
+
/**
|
|
87
|
+
* The page of a `frame` rule: one full-window `<iframe>` showing `src`, titled with the target's
|
|
88
|
+
* hostname. Both are HTML-escaped.
|
|
89
|
+
*/
|
|
90
|
+
export declare function framePage(src: string, target: string): string;
|
|
91
|
+
/** `&`, `<`, `>`, `"` and `'` as character references. */
|
|
92
|
+
export declare function escapeHtml(value: string): string;
|
package/dist/core/rules.js
CHANGED
|
@@ -143,7 +143,10 @@ function parseRuleLine(line) {
|
|
|
143
143
|
}
|
|
144
144
|
const rule = { source, target, status: 301, appendPath: false, keepQuery: false };
|
|
145
145
|
const status = columns[2]?.trim() ?? '';
|
|
146
|
-
if (status
|
|
146
|
+
if (status === 'frame') {
|
|
147
|
+
rule.status = 'frame';
|
|
148
|
+
}
|
|
149
|
+
else if (status !== '') {
|
|
147
150
|
if (!/^[+-]?\d+$/.test(status) || !STATUSES.has(Number(status))) {
|
|
148
151
|
return null;
|
|
149
152
|
}
|
|
@@ -263,9 +266,10 @@ export function matchExact(host, url) {
|
|
|
263
266
|
return host.exact.get(matchPath(url.pathname)) ?? null;
|
|
264
267
|
}
|
|
265
268
|
/**
|
|
266
|
-
* The `Location` of a rule for a request URL
|
|
267
|
-
* appended to the target's path without a double slash; with
|
|
268
|
-
* added with `?` or `&`, otherwise dropped. A fragment of the
|
|
269
|
+
* The `Location` of a rule for a request URL, and a `frame` rule's iframe `src`. With `appendPath`
|
|
270
|
+
* the escaped request path is appended to the target's path without a double slash; with
|
|
271
|
+
* `keepQuery` the request query is added with `?` or `&`, otherwise dropped. A fragment of the
|
|
272
|
+
* target stays last.
|
|
269
273
|
*/
|
|
270
274
|
export function buildLocation(rule, url) {
|
|
271
275
|
const hash = rule.target.indexOf('#');
|
|
@@ -283,3 +287,28 @@ export function buildLocation(rule, url) {
|
|
|
283
287
|
}
|
|
284
288
|
return hash < 0 ? location : `${location}#${rule.target.slice(hash + 1)}`;
|
|
285
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* The page of a `frame` rule: one full-window `<iframe>` showing `src`, titled with the target's
|
|
292
|
+
* hostname. Both are HTML-escaped.
|
|
293
|
+
*/
|
|
294
|
+
export function framePage(src, target) {
|
|
295
|
+
return [
|
|
296
|
+
'<!DOCTYPE html>',
|
|
297
|
+
'<html>',
|
|
298
|
+
'<head>',
|
|
299
|
+
'<meta charset="utf-8">',
|
|
300
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
301
|
+
`<title>${escapeHtml(new URL(target).hostname)}</title>`,
|
|
302
|
+
'<style>html,body{margin:0;height:100%;overflow:hidden}iframe{display:block;width:100%;height:100%;border:0}</style>',
|
|
303
|
+
'</head>',
|
|
304
|
+
'<body>',
|
|
305
|
+
`<iframe src="${escapeHtml(src)}" allowfullscreen></iframe>`,
|
|
306
|
+
'</body>',
|
|
307
|
+
'</html>',
|
|
308
|
+
'',
|
|
309
|
+
].join('\n');
|
|
310
|
+
}
|
|
311
|
+
/** `&`, `<`, `>`, `"` and `'` as character references. */
|
|
312
|
+
export function escapeHtml(value) {
|
|
313
|
+
return value.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
314
|
+
}
|
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.3.0";
|
|
5
|
+
export declare const USER_AGENT = "cloudflare-redirects/0.3.0";
|
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.3.0';
|
|
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.3.0",
|
|
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",
|