-
Notifications
You must be signed in to change notification settings - Fork 67
feat(serve-static): support absolute path #257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
8500774
cbfed14
2e9b4e9
e420e47
9a14f20
33f3ccf
22d69c0
54ecac3
d5c1002
52bd0e6
ee6159c
1737089
ff18658
f6f5b90
8f15053
d26e6ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,8 +1,8 @@ | ||||||
import type { Context, Env, MiddlewareHandler } from 'hono' | ||||||
import { getFilePath, getFilePathWithoutDefaultDocument } from 'hono/utils/filepath' | ||||||
import { getMimeType } from 'hono/utils/mime' | ||||||
import { createReadStream, lstatSync } from 'node:fs' | ||||||
import type { ReadStream, Stats } from 'node:fs' | ||||||
import { createReadStream, lstatSync } from 'node:fs' | ||||||
import { join, resolve } from 'node:path' | ||||||
|
||||||
export type ServeStaticOptions<E extends Env = Env> = { | ||||||
/** | ||||||
|
@@ -44,10 +44,6 @@ const createStreamBody = (stream: ReadStream) => { | |||||
return body | ||||||
} | ||||||
|
||||||
const addCurrentDirPrefix = (path: string) => { | ||||||
return `./${path}` | ||||||
} | ||||||
|
||||||
const getStats = (path: string) => { | ||||||
let stats: Stats | undefined | ||||||
try { | ||||||
|
@@ -60,6 +56,9 @@ const getStats = (path: string) => { | |||||
export const serveStatic = <E extends Env = any>( | ||||||
options: ServeStaticOptions<E> = { root: '' } | ||||||
): MiddlewareHandler<E> => { | ||||||
const optionRoot = options.root || '.' | ||||||
yusukebe marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
const optionPath = options.path | ||||||
|
||||||
return async (c, next) => { | ||||||
// Do nothing if Response is already set | ||||||
if (c.finalized) { | ||||||
|
@@ -69,35 +68,44 @@ export const serveStatic = <E extends Env = any>( | |||||
let filename: string | ||||||
|
||||||
try { | ||||||
filename = options.path ?? decodeURIComponent(c.req.path) | ||||||
const rawPath = optionPath ?? c.req.path | ||||||
// Prevent encoded path traversal attacks | ||||||
if (!optionPath) { | ||||||
const decodedPath = decodeURIComponent(rawPath) | ||||||
if (decodedPath.includes('..')) { | ||||||
await options.onNotFound?.(rawPath, c) | ||||||
return next() | ||||||
} | ||||||
} | ||||||
filename = optionPath ?? decodeURIComponent(c.req.path) | ||||||
} catch { | ||||||
await options.onNotFound?.(c.req.path, c) | ||||||
return next() | ||||||
} | ||||||
|
||||||
let path = getFilePathWithoutDefaultDocument({ | ||||||
filename: options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename, | ||||||
root: options.root, | ||||||
}) | ||||||
const requestPath = options.rewriteRequestPath | ||||||
? options.rewriteRequestPath(filename, c) | ||||||
: filename | ||||||
const rootResolved = resolve(optionRoot) | ||||||
let path: string | ||||||
|
||||||
if (path) { | ||||||
path = addCurrentDirPrefix(path) | ||||||
if (optionPath) { | ||||||
// Use path option directly if specified | ||||||
path = resolve(optionPath) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the current main branch, I think "root" is also used here.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @yusukebe Thank you! The behavior when specifying an absolute path in app.use('*', serveStatic({
root: 'public',
path: '/file.html',
})) Previously, it behaved as if it joined to root. If this is an intentional change in behavior, I think it's OK. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
} else { | ||||||
return next() | ||||||
// Build with root + requestPath | ||||||
path = resolve(join(optionRoot, requestPath)) | ||||||
} | ||||||
|
||||||
let stats = getStats(path) | ||||||
|
||||||
if (stats && stats.isDirectory()) { | ||||||
path = getFilePath({ | ||||||
filename: options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename, | ||||||
root: options.root, | ||||||
defaultDocument: options.index ?? 'index.html', | ||||||
}) | ||||||
const indexFile = options.index ?? 'index.html' | ||||||
path = resolve(join(path, indexFile)) | ||||||
|
||||||
if (path) { | ||||||
path = addCurrentDirPrefix(path) | ||||||
} else { | ||||||
// Security check: prevent path traversal attacks | ||||||
if (!optionPath && !path.startsWith(rootResolved)) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍
|
||||||
await options.onNotFound?.(path, c) | ||||||
return next() | ||||||
} | ||||||
|
||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To support Windows on CI.