|
1 | | -import net from 'node:net' |
2 | 1 | import type { Connect } from 'dep-types/connect' |
3 | | -import type { ResolvedConfig } from '../../config' |
| 2 | +import { hostValidationMiddleware as originalHostValidationMiddleware } from 'host-validation-middleware' |
4 | 3 | import type { ResolvedPreviewOptions, ResolvedServerOptions } from '../..' |
5 | 4 |
|
6 | | -const allowedHostsServerCache = new WeakMap<ResolvedConfig, Set<string>>() |
7 | | -const allowedHostsPreviewCache = new WeakMap<ResolvedConfig, Set<string>>() |
8 | | - |
9 | | -const isFileOrExtensionProtocolRE = /^(?:file|.+-extension):/i |
10 | | - |
11 | 5 | export function getAdditionalAllowedHosts( |
12 | 6 | resolvedServerOptions: Pick<ResolvedServerOptions, 'host' | 'hmr' | 'origin'>, |
13 | 7 | resolvedPreviewOptions: Pick<ResolvedPreviewOptions, 'host'>, |
@@ -49,132 +43,20 @@ export function getAdditionalAllowedHosts( |
49 | 43 | return list |
50 | 44 | } |
51 | 45 |
|
52 | | -// Based on webpack-dev-server's `checkHeader` function: https://github.com/webpack/webpack-dev-server/blob/v5.2.0/lib/Server.js#L3086 |
53 | | -// https://github.com/webpack/webpack-dev-server/blob/v5.2.0/LICENSE |
54 | | -export function isHostAllowedWithoutCache( |
| 46 | +export function hostValidationMiddleware( |
55 | 47 | allowedHosts: string[], |
56 | | - additionalAllowedHosts: string[], |
57 | | - host: string, |
58 | | -): boolean { |
59 | | - if (isFileOrExtensionProtocolRE.test(host)) { |
60 | | - return true |
61 | | - } |
62 | | - |
63 | | - // We don't care about malformed Host headers, |
64 | | - // because we only need to consider browser requests. |
65 | | - // Non-browser clients can send any value they want anyway. |
66 | | - // |
67 | | - // `Host = uri-host [ ":" port ]` |
68 | | - const trimmedHost = host.trim() |
69 | | - |
70 | | - // IPv6 |
71 | | - if (trimmedHost[0] === '[') { |
72 | | - const endIpv6 = trimmedHost.indexOf(']') |
73 | | - if (endIpv6 < 0) { |
74 | | - return false |
75 | | - } |
76 | | - // DNS rebinding attacks does not happen with IP addresses |
77 | | - return net.isIP(trimmedHost.slice(1, endIpv6)) === 6 |
78 | | - } |
79 | | - |
80 | | - // uri-host does not include ":" unless IPv6 address |
81 | | - const colonPos = trimmedHost.indexOf(':') |
82 | | - const hostname = |
83 | | - colonPos === -1 ? trimmedHost : trimmedHost.slice(0, colonPos) |
84 | | - |
85 | | - // DNS rebinding attacks does not happen with IP addresses |
86 | | - if (net.isIP(hostname) === 4) { |
87 | | - return true |
88 | | - } |
89 | | - |
90 | | - // allow localhost and .localhost by default as they always resolve to the loopback address |
91 | | - // https://datatracker.ietf.org/doc/html/rfc6761#section-6.3 |
92 | | - if (hostname === 'localhost' || hostname.endsWith('.localhost')) { |
93 | | - return true |
94 | | - } |
95 | | - |
96 | | - for (const additionalAllowedHost of additionalAllowedHosts) { |
97 | | - if (additionalAllowedHost === hostname) { |
98 | | - return true |
99 | | - } |
100 | | - } |
101 | | - |
102 | | - for (const allowedHost of allowedHosts) { |
103 | | - if (allowedHost === hostname) { |
104 | | - return true |
105 | | - } |
106 | | - |
107 | | - // allow all subdomains of it |
108 | | - // e.g. `.foo.example` will allow `foo.example`, `*.foo.example`, `*.*.foo.example`, etc |
109 | | - if ( |
110 | | - allowedHost[0] === '.' && |
111 | | - (allowedHost.slice(1) === hostname || hostname.endsWith(allowedHost)) |
112 | | - ) { |
113 | | - return true |
114 | | - } |
115 | | - } |
116 | | - |
117 | | - return false |
118 | | -} |
119 | | - |
120 | | -/** |
121 | | - * @param config resolved config |
122 | | - * @param isPreview whether it's for the preview server or not |
123 | | - * @param host the value of host header. See [RFC 9110 7.2](https://datatracker.ietf.org/doc/html/rfc9110#name-host-and-authority). |
124 | | - */ |
125 | | -export function isHostAllowed( |
126 | | - config: ResolvedConfig, |
127 | | - isPreview: boolean, |
128 | | - host: string, |
129 | | -): boolean { |
130 | | - const allowedHosts = isPreview |
131 | | - ? config.preview.allowedHosts |
132 | | - : config.server.allowedHosts |
133 | | - if (allowedHosts === true) { |
134 | | - return true |
135 | | - } |
136 | | - |
137 | | - const cache = isPreview ? allowedHostsPreviewCache : allowedHostsServerCache |
138 | | - if (!cache.has(config)) { |
139 | | - cache.set(config, new Set()) |
140 | | - } |
141 | | - |
142 | | - const cachedAllowedHosts = cache.get(config)! |
143 | | - if (cachedAllowedHosts.has(host)) { |
144 | | - return true |
145 | | - } |
146 | | - |
147 | | - const result = isHostAllowedWithoutCache( |
148 | | - allowedHosts, |
149 | | - config.additionalAllowedHosts, |
150 | | - host, |
151 | | - ) |
152 | | - if (result) { |
153 | | - cachedAllowedHosts.add(host) |
154 | | - } |
155 | | - return result |
156 | | -} |
157 | | - |
158 | | -export function hostCheckMiddleware( |
159 | | - config: ResolvedConfig, |
160 | 48 | isPreview: boolean, |
161 | 49 | ): Connect.NextHandleFunction { |
162 | | - // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` |
163 | | - return function viteHostCheckMiddleware(req, res, next) { |
164 | | - const hostHeader = req.headers.host |
165 | | - if (!hostHeader || !isHostAllowed(config, isPreview, hostHeader)) { |
166 | | - const hostname = hostHeader?.replace(/:\d+$/, '') |
| 50 | + return originalHostValidationMiddleware({ |
| 51 | + // Freeze the array to allow caching |
| 52 | + allowedHosts: Object.freeze([...allowedHosts]), |
| 53 | + generateErrorMessage(hostname) { |
167 | 54 | const hostnameWithQuotes = JSON.stringify(hostname) |
168 | 55 | const optionName = `${isPreview ? 'preview' : 'server'}.allowedHosts` |
169 | | - res.writeHead(403, { |
170 | | - 'Content-Type': 'text/plain', |
171 | | - }) |
172 | | - res.end( |
| 56 | + return ( |
173 | 57 | `Blocked request. This host (${hostnameWithQuotes}) is not allowed.\n` + |
174 | | - `To allow this host, add ${hostnameWithQuotes} to \`${optionName}\` in vite.config.js.`, |
| 58 | + `To allow this host, add ${hostnameWithQuotes} to \`${optionName}\` in vite.config.js.` |
175 | 59 | ) |
176 | | - return |
177 | | - } |
178 | | - return next() |
179 | | - } |
| 60 | + }, |
| 61 | + }) |
180 | 62 | } |
0 commit comments