Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds frontend support for a Virtual Host management feature in the OpenList admin panel. It introduces a new CRUD interface (list, add, edit, delete) for virtual hosts, following the same patterns established by the existing Metas, Shares, and Storages management pages. Virtual hosts allow mapping a domain to an internal OpenList path, with an optional Web Hosting mode for serving static HTML files directly.
Changes:
- Added a new
VirtualHostTypeScript interface and registered it in the type exports. - Created two new management pages (
VirtualHosts.tsxfor listing andAddOrEdit.tsxfor creating/editing virtual hosts), following existing CRUD page conventions. - Integrated the virtual hosts section into the admin side menu, route configuration, and English language translations.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/types/virtual_host.ts |
New VirtualHost interface definition with id, enabled, domain, path, and web_hosting fields. |
src/types/index.ts |
Re-exports the new VirtualHost type. |
src/pages/manage/virtual_hosts/VirtualHosts.tsx |
List page for virtual hosts with refresh, add, edit, and delete actions. |
src/pages/manage/virtual_hosts/AddOrEdit.tsx |
Add/Edit form for virtual hosts with enabled switch, domain input, path chooser, and web hosting toggle. |
src/pages/manage/sidemenu_items.tsx |
Adds "Virtual Hosts" entry to the admin side menu with a globe icon. |
src/pages/manage/routes.tsx |
Registers add and edit routes for virtual hosts. |
src/lang/en/virtual_hosts.json |
English translation strings for virtual host form labels and help text. |
src/lang/en/manage.json |
Adds "Virtual Hosts" label for the side menu. |
src/lang/en/entry.ts |
Registers the new virtual_hosts translation module. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
jyxjjj
previously approved these changes
May 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary / 摘要
实现虚拟主机(Virtual Host)功能的完整支持,包含两种工作模式:
模式一:路径重映射(Domain 填写,Web Hosting 关闭)
将指定域名的访问路径透明映射到后端真实路径,实现"伪静态"效果:
http://example.com/,地址栏保持不变,面包屑显示 🏠Home//api/fs/list、/api/fs/get)自动将请求路径映射到sharing.Files[0](如/123pan/Downloads)/p/、/d/)自动去掉 vhost 路径前缀,保持前端路径一致性,避免路径重复叠加模式二:Web Hosting(Domain 填写 + Web Hosting 开启)
将指定域名作为静态网站托管,直接返回 index.html 等静态文件内容:
index.html>index.htm>index.mhtml>index.md>default.htm>default.html>default.mhtml>default.md>readme.html>readme.htm>readme.mhtml>readme.mdforceContentTypeWriter包装器强制覆盖响应头中的 Content-Type,确保 HTML 文件在浏览器中正确渲染而非触发下载vhostInternalUser到请求 context,解决文件访问时的 401 权限问题.md文件服务端渲染预览(marked.js + DOMPurify).mhtml文件浏览器原生预览安全增强:
HasPrefix沙箱校验/api/admin/、/dav/、/s3/等管理路由Architecture / 架构原理图
请求处理流程
flowchart TD A[浏览器请求] --> B{gin Router} B --> C{VhostRouteGuard 中间件} C -->|Host 匹配 vhost 域名| D{WebHosting?} C -->|Host 不匹配| E[正常路由处理] C -->|"管理路由 /api/admin/ /dav/ /s3/"| F[404 拦截] D -->|WebHosting = true| G[virtualHostHandler] D -->|WebHosting = false| H[virtualHostHandler] G --> G1[访问码门禁 handleSharePwdGate] G1 -->|未通过| G2["密码输入页 / 403"] G1 -->|通过| G3[注入 vhostInternalUser] G3 --> G4[handleWebHosting] G4 --> G5{文件存在?} G5 -->|是| G6["serveWebHostingFile<br/>forceContentTypeWriter"] G5 -->|否| G7{indexCandidates 匹配?} G7 -->|命中| G6 G7 -->|全部未命中| G8["404"] H --> H1[访问码门禁 handleSharePwdGate] H1 -->|未通过| H2["密码输入页 / 403"] H1 -->|通过| H3[返回 SPA IndexHtml] H3 --> H4[浏览器加载 SPA] H4 --> H5["/api/fs/list 请求"] H5 --> H6[auth 中间件填入 user] H6 --> H7["applyVhostPathMapping<br/>路径重映射到 sharing.Files[0]"] H7 --> H8[返回文件列表]路径重映射模式数据流
sequenceDiagram participant Browser as 浏览器 participant Gin as Gin Router participant VHost as virtualHostHandler participant Auth as Auth 中间件 participant FSRead as fsread.go participant Storage as 存储后端 Browser->>Gin: GET http://vhost.test/ Gin->>VHost: NoRoute → virtualHostHandler VHost->>VHost: Host 匹配 sharing (WebHosting=false) VHost->>Browser: 200 SPA HTML (IndexHtml) Browser->>Gin: POST /api/fs/list (path: /) Gin->>Auth: Auth 中间件 → 填入 guest user Auth->>FSRead: FsListSplit handler FSRead->>FSRead: applyVhostPathMapping<br/>/ → /123pan/Downloads FSRead->>Storage: fs.List(ctx, /123pan/Downloads) Storage->>FSRead: 文件列表 FSRead->>FSRead: stripVhostPrefix 去掉前缀 FSRead->>Browser: 返回文件列表 Browser->>Gin: GET /p/filename sign=xxx Gin->>FSRead: PathParse applyDownVhostPathMapping FSRead->>FSRead: /filename → /123pan/Downloads/filename FSRead->>Storage: 获取文件链接 Storage->>Browser: 文件内容Web Hosting 模式数据流
sequenceDiagram participant Browser as 浏览器 participant Gin as Gin Router participant VHost as virtualHostHandler participant WH as handleWebHosting participant FS as internalfs participant Storage as 存储后端 Browser->>Gin: GET http://vhost.test/ Gin->>VHost: NoRoute → virtualHostHandler VHost->>VHost: Host 匹配 sharing (WebHosting=true) VHost->>VHost: 注入 vhostInternalUser VHost->>WH: handleWebHosting WH->>FS: internalfs.Get /root/index.html FS->>Storage: 查询文件 Storage->>FS: 文件对象 FS->>WH: obj 非目录 WH->>FS: internalfs.Link /root/index.html FS->>Storage: 获取下载链接 Storage->>FS: link WH->>WH: forceContentTypeWriter Content-Type text/html WH->>Browser: 200 HTML 内容安全架构
flowchart LR subgraph 入口防护 A1[VhostRouteGuard<br/>阻止管理路由] A2[Domain 格式校验<br/>ToLower + 正则] end subgraph 访问控制 B1[handleSharePwdGate<br/>访问码门禁] B2[HMAC-SHA256 Cookie<br/>非明文存储] B3[ConstantTimeCompare<br/>防时序侧信道] end subgraph 沙箱隔离 C1[HasPrefix 路径校验<br/>防穿越] C2[vhostInternalUser<br/>受限系统用户] C3[sharing.Files 0 根目录<br/>不可逃逸] end subgraph 响应安全 D1[X-Content-Type-Options<br/>nosniff] D2[Referrer-Policy<br/>strict-origin] D3[Cache-Control<br/>按类型区分] end A1 --> B1 --> C1 --> D1Motivation and Context / 背景与动机
原有虚拟主机功能存在以下问题:
too many redirects错误history.replaceState注入脚本修改地址栏,不符合"伪静态"语义Content-Type覆盖了正确的 MIME 类型,导致index.html被当作application/octet-stream下载forceContentTypeWriter在响应阶段强制覆盖正确的 Content-Typeinternalfs.Get/Link时,context 中缺少用户信息vhostInternalUser(非 nil、非 Disabled 的系统用户)/p/链接经过中间件再次映射后路径翻倍stripVhostPrefix在生成链接前去掉前缀;VhostPrefixKey通过 context 传递(*model.User)(nil)到 context,下游 hook 可能解引用 panicvhostInternalUser实例(Role=GUEST, Permission=0x7FFF)Vhost.Test,浏览器请求vhost.test,匹配失败ToLower + TrimSpace归一化,查询时同样归一化/api/admin/、/dav/等仍可访问VhostRouteGuard中间件拦截Description / 详细描述
核心设计决策
复用 Sharing 模型:虚拟主机配置直接挂载在
SharingDB上(新增Domain+WebHosting字段),无需新建表,复用已有的过期、禁用、访问计数等能力。双模式分发:
virtualHostHandler作为 gin NoRoute 的 catch-all handler,根据sharing.WebHosting字段决定走 Web Hosting 还是路径重映射。vhostInternalUser:定义一个
ID=0, Username="_vhost_internal", Role=GUEST, Permission=0x7FFF, BasePath="/", Disabled=false的系统用户,仅在 Web Hosting 模式注入 context。实际访问范围由handleWebHosting的HasPrefix沙箱保证。缓存策略:
domainSharingCache按域名缓存 sharing 对象(正向 1h,负向 5min),通过singleflight防击穿。Create/Update/Delete 成功后才失效缓存。访问码门禁:cookie 存储
HMAC-SHA256(sharingID + ":" + pwd, salt)而非明文密码,即使 cookie 泄露也无法反推原始密码。主要变更列表:
internal/model/sharing.go:新增Domain、WebHosting字段及ValidForVhost()方法internal/op/sharing.go:新增GetSharingByDomain()带缓存查询(含负缓存防穿透)internal/db/sharing.go:新增GetSharingByDomain()DB 查询 + Create/Update 时域名唯一性校验server/static/static.go:virtualHostHandler实现双模式分发 +handleWebHosting+forceContentTypeWriterserver/static/share_pwd.go:新增 访问码门禁完整实现server/static/render.go:新增 Markdown 服务端预览渲染server/handles/fsread.go:applyVhostPathMappingAPI 路径重映射 +stripVhostPrefix下载链接修正server/handles/sharing.go:normalizeDomain()域名归一化与格式校验server/middlewares/down.go:applyDownVhostPathMapping下载路由路径重映射server/middlewares/vhost.go:新增VhostRouteGuard路由守卫中间件server/common/common.go:新增StripHostPort工具函数internal/conf/const.go:新增VhostPrefixKeycontext keyThis PR has breaking changes.
/ 此 PR 包含破坏性变更。
This PR changes public API, config, storage format, or migration behavior.
/ 此 PR 修改了公开 API、配置、存储格式或迁移行为。
This PR requires corresponding changes in related repositories.
/ 此 PR 需要关联仓库同步修改。
破坏性变更说明:
SharingDB新增Domain string和WebHosting bool字段,AutoMigrate 会自动添加列Domain字段使用普通index(非 uniqueIndex),唯一性由应用层校验,避免已有空字符串记录在 MySQL 下冲突Related repository PRs / 关联仓库 PR:
dev-vhost分支(新增 Domain / WebHosting UI 字段)Related Issues / 关联 Issue
Relates to virtual host / web hosting feature request.
Testing / 测试
go test ./...测试步骤:
localhost,映射路径/123pan/Downloadshttp://localhost:5244/,验证:http://localhost:5244/不变 ✅/123pan/Downloads的内容 ✅/subdir,文件列表正确显示/123pan/Downloads/subdir的内容 ✅/p/filename,不含 vhost 路径前缀 ✅index.html到映射路径:/api/admin/setting/list:Checklist / 检查清单
/ 我已阅读 CONTRIBUTING。
/ 我确认此贡献符合仓库许可证、贡献规范和行为准则。
gofmt,go fmt, orprettierwhere applicable./ 我已按适用情况使用
gofmt、go fmt或prettier格式化变更代码。/ 我已在适用情况下请求相关维护者或代码所有者审查。
AI Disclosure / AI 使用声明
/ 此 PR 包含 AI 辅助内容。
Tools used / 使用工具:
Usage scope / 使用范围:
/ 我已审核并验证此 PR 中的所有 AI 辅助内容。
Co-Authored-Byattribution./ 我已确保所有 AI 辅助提交都包含
Co-Authored-By归属信息。/ 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。