/
link.ts
206 lines (179 loc) · 5.58 KB
/
link.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import { detectBot, getFinalUrl, parse } from "@/lib/middleware/utils";
import { recordClick } from "@/lib/tinybird";
import { formatRedisLink, redis } from "@/lib/upstash";
import {
DUB_DEMO_LINKS,
DUB_HEADERS,
LEGAL_WORKSPACE_ID,
LOCALHOST_GEO_DATA,
punyEncode,
} from "@dub/utils";
import {
NextFetchEvent,
NextRequest,
NextResponse,
userAgent,
} from "next/server";
import { isBlacklistedReferrer } from "../edge-config";
import { getLinkViaEdge } from "../planetscale";
import { RedisLinkProps } from "../types";
export default async function LinkMiddleware(
req: NextRequest,
ev: NextFetchEvent,
) {
let { domain, fullKey: key } = parse(req);
if (!domain || !key) {
return NextResponse.next();
}
// encode the key to ascii
// links on Dub are case insensitive by default
key = punyEncode(key.toLowerCase());
const demoLink = DUB_DEMO_LINKS.find(
(l) => l.domain === domain && l.key === key,
);
// if it's a demo link, block bad referrers in production
if (
process.env.NODE_ENV !== "development" &&
demoLink &&
(await isBlacklistedReferrer(req.headers.get("referer")))
) {
return new Response("Don't DDoS me pls 🥺", { status: 429 });
}
const inspectMode = key.endsWith("+");
// if inspect mode is enabled, remove the trailing `+` from the key
if (inspectMode) {
key = key.slice(0, -1);
}
let link = await redis.hget<RedisLinkProps>(domain, key);
if (!link) {
const linkData = await getLinkViaEdge(domain, key);
if (!linkData) {
// short link not found, redirect to root
// TODO: log 404s (https://github.com/dubinc/dub/issues/559)
return NextResponse.redirect(new URL("/", req.url), {
...DUB_HEADERS,
status: 302,
});
}
// format link to fit the RedisLinkProps interface
link = await formatRedisLink(linkData as any);
ev.waitUntil(
redis.hset(domain, {
[key]: link,
}),
);
}
const {
id,
url,
password,
proxy,
rewrite,
iframeable,
expiresAt,
ios,
android,
geo,
expiredUrl,
} = link;
// only show inspect modal if the link is not password protected
if (inspectMode && !password) {
return NextResponse.rewrite(
new URL(`/inspect/${domain}/${encodeURIComponent(key)}+`, req.url),
DUB_HEADERS,
);
}
// if the link is password protected
if (password) {
const pw = req.nextUrl.searchParams.get("pw");
// rewrite to auth page (/password/[domain]/[key]) if:
// - no `pw` param is provided
// - the `pw` param is incorrect
// this will also ensure that no clicks are tracked unless the password is correct
if (!pw || (await getLinkViaEdge(domain, key))?.password !== pw) {
return NextResponse.rewrite(
new URL(`/password/${domain}/${encodeURIComponent(key)}`, req.url),
DUB_HEADERS,
);
} else if (pw) {
// strip it from the URL if it's correct
req.nextUrl.searchParams.delete("pw");
}
}
// if the link is banned
if (link.projectId === LEGAL_WORKSPACE_ID) {
return NextResponse.rewrite(new URL("/banned", req.url), DUB_HEADERS);
}
// if the link has expired
if (expiresAt && new Date(expiresAt) < new Date()) {
if (expiredUrl) {
return NextResponse.redirect(expiredUrl, DUB_HEADERS);
} else {
return NextResponse.rewrite(
new URL(`/expired/${domain}`, req.url),
DUB_HEADERS,
);
}
}
const searchParams = req.nextUrl.searchParams;
// only track the click when there is no `dub-no-track` header or query param
if (
!(
req.headers.get("dub-no-track") ||
searchParams.get("dub-no-track") === "1"
)
) {
ev.waitUntil(recordClick({ req, id, url: getFinalUrl(url, { req }) }));
}
const isBot = detectBot(req);
const { country } =
process.env.VERCEL === "1" && req.geo ? req.geo : LOCALHOST_GEO_DATA;
// rewrite to proxy page (/proxy/[domain]/[key]) if it's a bot and proxy is enabled
if (isBot && proxy) {
return NextResponse.rewrite(
new URL(`/proxy/${domain}/${encodeURIComponent(key)}`, req.url),
DUB_HEADERS,
);
// rewrite to mailto page if the link is a mailto link
} else if (url.startsWith("mailto:")) {
return NextResponse.rewrite(
new URL(`/mailto/${encodeURIComponent(url)}`, req.url),
DUB_HEADERS,
);
// rewrite to target URL if link cloaking is enabled
} else if (rewrite) {
if (iframeable) {
return NextResponse.rewrite(
new URL(`/cloaked/${encodeURIComponent(url)}`, req.url),
DUB_HEADERS,
);
} else {
// if link is not iframeable, use Next.js rewrite instead
return NextResponse.rewrite(url, DUB_HEADERS);
}
// redirect to iOS link if it is specified and the user is on an iOS device
} else if (ios && userAgent(req).os?.name === "iOS") {
return NextResponse.redirect(getFinalUrl(ios, { req }), {
...DUB_HEADERS,
status: 302,
});
// redirect to Android link if it is specified and the user is on an Android device
} else if (android && userAgent(req).os?.name === "Android") {
return NextResponse.redirect(getFinalUrl(android, { req }), {
...DUB_HEADERS,
status: 302,
});
// redirect to geo-specific link if it is specified and the user is in the specified country
} else if (geo && country && country in geo) {
return NextResponse.redirect(getFinalUrl(geo[country], { req }), {
...DUB_HEADERS,
status: 302,
});
// regular redirect
} else {
return NextResponse.redirect(getFinalUrl(url, { req }), {
...DUB_HEADERS,
status: 302,
});
}
}