Skip to content

Commit

Permalink
chore: join URL segments more safely (#10590)
Browse files Browse the repository at this point in the history
  • Loading branch information
benmccann committed Oct 27, 2022
1 parent f787a60 commit 675bf07
Show file tree
Hide file tree
Showing 8 changed files with 46 additions and 26 deletions.
10 changes: 8 additions & 2 deletions packages/vite/src/node/build.ts
Expand Up @@ -27,7 +27,13 @@ import { isDepsOptimizerEnabled, resolveConfig } from './config'
import { buildReporterPlugin } from './plugins/reporter'
import { buildEsbuildPlugin } from './plugins/esbuild'
import { terserPlugin } from './plugins/terser'
import { copyDir, emptyDir, lookupFile, normalizePath } from './utils'
import {
copyDir,
emptyDir,
joinUrlSegments,
lookupFile,
normalizePath
} from './utils'
import { manifestPlugin } from './plugins/manifest'
import type { Logger } from './logger'
import { dataURIPlugin } from './plugins/dataUri'
Expand Down Expand Up @@ -1071,7 +1077,7 @@ export function toOutputFilePathInJS(
if (relative && !config.build.ssr) {
return toRelative(filename, hostId)
}
return config.base + filename
return joinUrlSegments(config.base, filename)
}

export function createToImportMetaURLBasedRelativeRuntime(
Expand Down
9 changes: 4 additions & 5 deletions packages/vite/src/node/plugins/asset.ts
Expand Up @@ -19,7 +19,7 @@ import {
} from '../build'
import type { Plugin } from '../plugin'
import type { ResolvedConfig } from '../config'
import { cleanUrl, getHash, normalizePath } from '../utils'
import { cleanUrl, getHash, joinUrlSegments, normalizePath } from '../utils'
import { FS_PREFIX } from '../constants'

export const assetUrlRE = /__VITE_ASSET__([a-z\d]{8})__(?:\$_(.*?)__)?/g
Expand Down Expand Up @@ -249,9 +249,8 @@ function fileToDevUrl(id: string, config: ResolvedConfig) {
// (this is special handled by the serve static middleware
rtn = path.posix.join(FS_PREFIX + id)
}
const origin = config.server?.origin ?? ''
const devBase = config.base
return origin + devBase + rtn.replace(/^\//, '')
const base = joinUrlSegments(config.server?.origin ?? '', config.base)
return joinUrlSegments(base, rtn.replace(/^\//, ''))
}

export function getAssetFilename(
Expand Down Expand Up @@ -396,7 +395,7 @@ export function publicFileToBuiltUrl(
): string {
if (config.command !== 'build') {
// We don't need relative base or renderBuiltUrl support during dev
return config.base + url.slice(1)
return joinUrlSegments(config.base, url)
}
const hash = getHash(url)
let cache = publicAssetUrlCache.get(config)
Expand Down
8 changes: 4 additions & 4 deletions packages/vite/src/node/plugins/css.ts
Expand Up @@ -39,6 +39,7 @@ import {
isDataUrl,
isExternalUrl,
isObject,
joinUrlSegments,
normalizePath,
parseRequest,
processSrcSet,
Expand Down Expand Up @@ -211,7 +212,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
if (encodePublicUrlsInCSS(config)) {
return publicFileToBuiltUrl(url, config)
} else {
return config.base + url.slice(1)
return joinUrlSegments(config.base, url)
}
}
const resolved = await resolveUrl(url, importer)
Expand Down Expand Up @@ -249,7 +250,6 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
// server only logic for handling CSS @import dependency hmr
const { moduleGraph } = server
const thisModule = moduleGraph.getModuleById(id)
const devBase = config.base
if (thisModule) {
// CSS modules cannot self-accept since it exports values
const isSelfAccepting =
Expand All @@ -258,6 +258,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
// record deps in the module graph so edits to @import css can trigger
// main import to hot update
const depModules = new Set<string | ModuleNode>()
const devBase = config.base
for (const file of deps) {
depModules.add(
isCSSRequest(file)
Expand Down Expand Up @@ -387,10 +388,9 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
}

const cssContent = await getContentWithSourcemap(css)
const devBase = config.base
const code = [
`import { updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle } from ${JSON.stringify(
path.posix.join(devBase, CLIENT_PUBLIC_PATH)
path.posix.join(config.base, CLIENT_PUBLIC_PATH)
)}`,
`const __vite__id = ${JSON.stringify(id)}`,
`const __vite__css = ${JSON.stringify(cssContent)}`,
Expand Down
7 changes: 3 additions & 4 deletions packages/vite/src/node/server/index.ts
Expand Up @@ -547,8 +547,7 @@ export async function createServer(
}

// base
const devBase = config.base
if (devBase !== '/') {
if (config.base !== '/') {
middlewares.use(baseMiddleware(server))
}

Expand Down Expand Up @@ -652,7 +651,6 @@ async function startServer(

const protocol = options.https ? 'https' : 'http'
const info = server.config.logger.info
const devBase = server.config.base

const serverPort = await httpServerStart(httpServer, {
port,
Expand Down Expand Up @@ -681,7 +679,8 @@ async function startServer(
}

if (options.open && !isRestart) {
const path = typeof options.open === 'string' ? options.open : devBase
const path =
typeof options.open === 'string' ? options.open : server.config.base
openBrowser(
path.startsWith('http')
? path
Expand Down
9 changes: 5 additions & 4 deletions packages/vite/src/node/server/middlewares/base.ts
@@ -1,12 +1,13 @@
import type { Connect } from 'dep-types/connect'
import type { ViteDevServer } from '..'
import { joinUrlSegments } from '../../utils'

// this middleware is only active when (config.base !== '/')

export function baseMiddleware({
config
}: ViteDevServer): Connect.NextHandleFunction {
const devBase = config.base
const devBase = config.base.endsWith('/') ? config.base : config.base + '/'

// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return function viteBaseMiddleware(req, res, next) {
Expand All @@ -29,18 +30,18 @@ export function baseMiddleware({
if (path === '/' || path === '/index.html') {
// redirect root visit to based url with search and hash
res.writeHead(302, {
Location: devBase + (parsed.search || '') + (parsed.hash || '')
Location: config.base + (parsed.search || '') + (parsed.hash || '')
})
res.end()
return
} else if (req.headers.accept?.includes('text/html')) {
// non-based page visit
const redirectPath = devBase + url.slice(1)
const redirectPath = joinUrlSegments(config.base, url)
res.writeHead(404, {
'Content-Type': 'text/html'
})
res.end(
`The server is configured with a public base URL of ${devBase} - ` +
`The server is configured with a public base URL of ${config.base} - ` +
`did you mean to visit <a href="${redirectPath}">${redirectPath}</a> instead?`
)
return
Expand Down
6 changes: 4 additions & 2 deletions packages/vite/src/node/server/middlewares/indexHtml.ts
Expand Up @@ -26,6 +26,7 @@ import {
ensureWatchedFile,
fsPathFromId,
injectQuery,
joinUrlSegments,
normalizePath,
processSrcSetSync,
wrapId
Expand Down Expand Up @@ -93,7 +94,8 @@ const processNodeUrl = (
const devBase = config.base
if (startsWithSingleSlashRE.test(url)) {
// prefix with base (dev only, base is never relative)
overwriteAttrValue(s, sourceCodeLocation, devBase + url.slice(1))
const fullUrl = joinUrlSegments(devBase, url)
overwriteAttrValue(s, sourceCodeLocation, fullUrl)
} else if (
url.startsWith('.') &&
originalUrl &&
Expand Down Expand Up @@ -132,7 +134,7 @@ const devHtmlHook: IndexHtmlTransformHook = async (
const trailingSlash = htmlPath.endsWith('/')
if (!trailingSlash && fs.existsSync(filename)) {
proxyModulePath = htmlPath
proxyModuleUrl = base + htmlPath.slice(1)
proxyModuleUrl = joinUrlSegments(base, htmlPath)
} else {
// There are users of vite.transformIndexHtml calling it with url '/'
// for SSR integrations #7993, filename is root for this case
Expand Down
10 changes: 5 additions & 5 deletions packages/vite/src/node/ssr/ssrManifestPlugin.ts
Expand Up @@ -5,7 +5,7 @@ import type { OutputChunk } from 'rollup'
import type { ResolvedConfig } from '..'
import type { Plugin } from '../plugin'
import { preloadMethod } from '../plugins/importAnalysisBuild'
import { normalizePath } from '../utils'
import { joinUrlSegments, normalizePath } from '../utils'

export function ssrManifestPlugin(config: ResolvedConfig): Plugin {
// module id => preload assets mapping
Expand All @@ -23,15 +23,15 @@ export function ssrManifestPlugin(config: ResolvedConfig): Plugin {
const mappedChunks =
ssrManifest[normalizedId] ?? (ssrManifest[normalizedId] = [])
if (!chunk.isEntry) {
mappedChunks.push(base + chunk.fileName)
mappedChunks.push(joinUrlSegments(base, chunk.fileName))
// <link> tags for entry chunks are already generated in static HTML,
// so we only need to record info for non-entry chunks.
chunk.viteMetadata.importedCss.forEach((file) => {
mappedChunks.push(base + file)
mappedChunks.push(joinUrlSegments(base, file))
})
}
chunk.viteMetadata.importedAssets.forEach((file) => {
mappedChunks.push(base + file)
mappedChunks.push(joinUrlSegments(base, file))
})
}
if (chunk.code.includes(preloadMethod)) {
Expand Down Expand Up @@ -59,7 +59,7 @@ export function ssrManifestPlugin(config: ResolvedConfig): Plugin {
const chunk = bundle[filename] as OutputChunk | undefined
if (chunk) {
chunk.viteMetadata.importedCss.forEach((file) => {
deps.push(join(base, file)) // TODO:base
deps.push(joinUrlSegments(base, file)) // TODO:base
})
chunk.imports.forEach(addDeps)
}
Expand Down
13 changes: 13 additions & 0 deletions packages/vite/src/node/utils.ts
Expand Up @@ -1191,3 +1191,16 @@ export const isNonDriveRelativeAbsolutePath = (p: string): boolean => {
if (!isWindows) return p.startsWith('/')
return windowsDrivePathPrefixRE.test(p)
}

export function joinUrlSegments(a: string, b: string): string {
if (!a || !b) {
return a || b || ''
}
if (a.endsWith('/')) {
a = a.substring(0, a.length - 1)
}
if (!b.startsWith('/')) {
b = '/' + b
}
return a + b
}

0 comments on commit 675bf07

Please sign in to comment.