Skip to content

Commit

Permalink
fix #3208: avoid protocol-relative url redirects
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jul 10, 2023
1 parent 5949e5c commit 838f90d
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 1 deletion.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

* Fix a redirect edge case in esbuild's development server ([#3208](https://github.com/evanw/esbuild/issues/3208))

The development server canonicalizes directory URLs by adding a trailing slash. For example, visiting `/about` redirects to `/about/` if `/about/index.html` would be served. However, if the requested path begins with two slashes, then the redirect incorrectly turned into a protocol-relative URL. For example, visiting `//about` redirected to `//about/` which the browser turns into `http://about/`. This release fixes the bug by canonicalizing the URL path when doing this redirect.

## 0.18.11

* Fix a TypeScript code generation edge case ([#3199](https://github.com/evanw/esbuild/issues/3199))
Expand Down
2 changes: 1 addition & 1 deletion pkg/api/serve_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ func (h *apiHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {

// Redirect to a trailing slash for directories
if kind == fs.DirEntry && !strings.HasSuffix(req.URL.Path, "/") {
res.Header().Set("Location", req.URL.Path+"/")
res.Header().Set("Location", path.Clean(req.URL.Path)+"/")
go h.notifyRequest(time.Since(start), req, http.StatusFound)
res.WriteHeader(http.StatusFound)
maybeWriteResponseBody(nil)
Expand Down
88 changes: 88 additions & 0 deletions scripts/js-api-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -3801,6 +3801,15 @@ function fetchHTTPS(host, port, path, { certfile }) {
})
}

function partialFetch(host, port, path, { headers, method = 'GET' } = {}) {
return new Promise((resolve, reject) => {
http.request({ method, host, port, path, headers }, res => {
resolve(res)
res.socket.destroy()
}).on('error', reject).end()
})
}

const makeEventStream = (host, port, path) => {
return new Promise((resolve, reject) => {
const headers = {
Expand Down Expand Up @@ -4356,6 +4365,85 @@ let serveTests = {
}
},

async serveSlashRedirect({ esbuild, testDir }) {
const nestedDir = path.join(testDir, 'nested', 'dir')
const index = path.join(nestedDir, 'index.html')
await mkdirAsync(nestedDir, { recursive: true })
await writeFileAsync(index, `<!doctype html>`)

const context = await esbuild.context({});
try {
const result = await context.serve({
host: '127.0.0.1',
servedir: testDir,
})
assert.strictEqual(result.host, '127.0.0.1');
assert.strictEqual(typeof result.port, 'number');

// With a trailing slash
{
const buffer = await fetch(result.host, result.port, '/nested/dir/index.html')
assert.strictEqual(buffer.toString(), `<!doctype html>`)
}
{
const buffer = await fetch(result.host, result.port, '/nested/dir/')
assert.strictEqual(buffer.toString(), `<!doctype html>`)
}
{
const buffer = await fetch(result.host, result.port, '/nested/./dir/')
assert.strictEqual(buffer.toString(), `<!doctype html>`)
}
{
const buffer = await fetch(result.host, result.port, '/./nested/./dir/./')
assert.strictEqual(buffer.toString(), `<!doctype html>`)
}
{
const buffer = await fetch(result.host, result.port, '/nested/dir//')
assert.strictEqual(buffer.toString(), `<!doctype html>`)
}
{
const buffer = await fetch(result.host, result.port, '/nested//dir/')
assert.strictEqual(buffer.toString(), `<!doctype html>`)
}

// Without a trailing slash
{
const res = await partialFetch(result.host, result.port, '/nested')
assert.strictEqual(res.statusCode, 302)
assert.strictEqual(res.headers.location, '/nested/')
}
{
const res = await partialFetch(result.host, result.port, '/nested/dir')
assert.strictEqual(res.statusCode, 302)
assert.strictEqual(res.headers.location, '/nested/dir/')
}
{
const res = await partialFetch(result.host, result.port, '/nested//dir')
assert.strictEqual(res.statusCode, 302)
assert.strictEqual(res.headers.location, '/nested/dir/')
}

// With leading double slashes (looks like a protocol-relative URL)
{
const res = await partialFetch(result.host, result.port, '//nested')
assert.strictEqual(res.statusCode, 302)
assert.strictEqual(res.headers.location, '/nested/')
}
{
const res = await partialFetch(result.host, result.port, '//nested/dir')
assert.strictEqual(res.statusCode, 302)
assert.strictEqual(res.headers.location, '/nested/dir/')
}
{
const res = await partialFetch(result.host, result.port, '//nested//dir')
assert.strictEqual(res.statusCode, 302)
assert.strictEqual(res.headers.location, '/nested/dir/')
}
} finally {
await context.dispose();
}
},

async serveOutfile({ esbuild, testDir }) {
const input = path.join(testDir, 'in.js')
const outfile = path.join(testDir, 'out.js')
Expand Down

0 comments on commit 838f90d

Please sign in to comment.