Skip to content

feat(func): support virtual host via sharing record#398

Closed
PIKACHUIM wants to merge 4 commits into
mainfrom
dev-vhost
Closed

feat(func): support virtual host via sharing record#398
PIKACHUIM wants to merge 4 commits into
mainfrom
dev-vhost

Conversation

@PIKACHUIM
Copy link
Copy Markdown
Member

@PIKACHUIM PIKACHUIM commented Mar 9, 2026

Summary / 摘要

实现虚拟主机(Virtual Host)功能的完整支持,包含两种工作模式:

模式一:路径重映射(Domain 填写,Web Hosting 关闭)

将指定域名的访问路径透明映射到后端真实路径,实现"伪静态"效果:

  • 访问 http://example.com/,地址栏保持不变,面包屑显示 🏠Home/
  • 后端 API(/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.md
  • 全部未命中时返回 404
  • 通过 forceContentTypeWriter 包装器强制覆盖响应头中的 Content-Type,确保 HTML 文件在浏览器中正确渲染而非触发下载
  • 注入 vhostInternalUser 到请求 context,解决文件访问时的 401 权限问题
  • 支持 .md 文件服务端渲染预览(marked.js + DOMPurify)
  • 支持 .mhtml 文件浏览器原生预览

安全增强:

  • 访问码门禁:sharing 设置密码时,vhost 访问需先通过密码校验(内嵌密码输入页 + HMAC token cookie)
  • 路径穿越防护:所有路径映射均做 HasPrefix 沙箱校验
  • 路由守卫:vhost 域名下阻止 /api/admin//dav//s3/ 等管理路由
  • Domain 格式校验:入库时强制 ToLower + 正则校验合法 hostname
  • Cookie 安全:存储 HMAC-SHA256 token 而非明文密码,HttpOnly + SameSite=Lax + Secure(HTTPS)

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[返回文件列表]
Loading

路径重映射模式数据流

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: 文件内容
Loading

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 内容
Loading

安全架构

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 --> D1
Loading

Motivation and Context / 背景与动机

原有虚拟主机功能存在以下问题:

问题 原因 本 PR 解决方案
无限重定向 通过 302 跳转实现路径映射,导致 too many redirects 错误 改为服务端直接返回 SPA HTML,不做任何重定向
地址栏变化 使用 history.replaceState 注入脚本修改地址栏,不符合"伪静态"语义 地址栏始终保持用户输入的 URL,后端透明映射
Web Hosting 文件被下载 代理上游响应头中的 Content-Type 覆盖了正确的 MIME 类型,导致 index.html 被当作 application/octet-stream 下载 forceContentTypeWriter 在响应阶段强制覆盖正确的 Content-Type
401 权限错误 Web Hosting 模式下调用 internalfs.Get/Link 时,context 中缺少用户信息 注入 vhostInternalUser(非 nil、非 Disabled 的系统用户)
下载链接路径重复 前端拿到的是真实路径(含 vhost 前缀),生成的 /p/ 链接经过中间件再次映射后路径翻倍 stripVhostPrefix 在生成链接前去掉前缀;VhostPrefixKey 通过 context 传递
nil user panic 风险 旧实现注入 (*model.User)(nil) 到 context,下游 hook 可能解引用 panic 改为注入 vhostInternalUser 实例(Role=GUEST, Permission=0x7FFF)
域名大小写不匹配 用户填写 Vhost.Test,浏览器请求 vhost.test,匹配失败 入库时 ToLower + TrimSpace 归一化,查询时同样归一化
缓存竞态 缓存在 DB 写入前失效,并发窗口下负缓存覆盖正向数据 缓存失效移到 DB 写成功之后
管理路由暴露 vhost 域名下 /api/admin//dav/ 等仍可访问 VhostRouteGuard 中间件拦截

Description / 详细描述

核心设计决策

  1. 复用 Sharing 模型:虚拟主机配置直接挂载在 SharingDB 上(新增 Domain + WebHosting 字段),无需新建表,复用已有的过期、禁用、访问计数等能力。

  2. 双模式分发virtualHostHandler 作为 gin NoRoute 的 catch-all handler,根据 sharing.WebHosting 字段决定走 Web Hosting 还是路径重映射。

  3. vhostInternalUser:定义一个 ID=0, Username="_vhost_internal", Role=GUEST, Permission=0x7FFF, BasePath="/", Disabled=false 的系统用户,仅在 Web Hosting 模式注入 context。实际访问范围由 handleWebHostingHasPrefix 沙箱保证。

  4. 缓存策略domainSharingCache 按域名缓存 sharing 对象(正向 1h,负向 5min),通过 singleflight 防击穿。Create/Update/Delete 成功后才失效缓存。

  5. 访问码门禁:cookie 存储 HMAC-SHA256(sharingID + ":" + pwd, salt) 而非明文密码,即使 cookie 泄露也无法反推原始密码。

主要变更列表:

  • internal/model/sharing.go:新增 DomainWebHosting 字段及 ValidForVhost() 方法

  • internal/op/sharing.go:新增 GetSharingByDomain() 带缓存查询(含负缓存防穿透)

  • internal/db/sharing.go:新增 GetSharingByDomain() DB 查询 + Create/Update 时域名唯一性校验

  • server/static/static.govirtualHostHandler 实现双模式分发 + handleWebHosting + forceContentTypeWriter

  • server/static/share_pwd.go新增 访问码门禁完整实现

  • server/static/render.go新增 Markdown 服务端预览渲染

  • server/handles/fsread.goapplyVhostPathMapping API 路径重映射 + stripVhostPrefix 下载链接修正

  • server/handles/sharing.gonormalizeDomain() 域名归一化与格式校验

  • server/middlewares/down.goapplyDownVhostPathMapping 下载路由路径重映射

  • server/middlewares/vhost.go新增 VhostRouteGuard 路由守卫中间件

  • server/common/common.go新增 StripHostPort 工具函数

  • internal/conf/const.go:新增 VhostPrefixKey context key

  • This 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 stringWebHosting bool 字段,AutoMigrate 会自动添加列
  • Domain 字段使用普通 index(非 uniqueIndex),唯一性由应用层校验,避免已有空字符串记录在 MySQL 下冲突

Related repository PRs / 关联仓库 PR:

  • OpenList-Frontend: dev-vhost 分支(新增 Domain / WebHosting UI 字段)
  • OpenList-Docs: 待补充

Related Issues / 关联 Issue

Relates to virtual host / web hosting feature request.

Testing / 测试

  • go test ./...
  • Manual test / 手动测试:

测试步骤:

  1. 配置虚拟主机绑定域名 localhost,映射路径 /123pan/Downloads
  2. 访问 http://localhost:5244/,验证:
    • 地址栏保持 http://localhost:5244/ 不变 ✅
    • 面包屑显示 🏠Home/ ✅
    • 文件列表正确显示 /123pan/Downloads 的内容 ✅
    • 点击子目录,地址栏变为 /subdir,文件列表正确显示 /123pan/Downloads/subdir 的内容 ✅
    • 下载按钮链接为 /p/filename,不含 vhost 路径前缀 ✅
  3. 开启 Web Hosting,上传 index.html 到映射路径:
    • 访问域名,浏览器直接渲染 HTML 页面,不触发下载 ✅
    • 无 401 权限错误 ✅
    • 访问不存在的路径返回 404 ✅
  4. 设置分享密码后访问 vhost 域名:
    • 显示密码输入页 ✅
    • 输入正确密码后 302 重定向并设置 cookie ✅
    • 后续访问免输入 ✅
  5. 通过 vhost 域名访问 /api/admin/setting/list
    • 返回 404(路由守卫拦截)✅

Checklist / 检查清单

  • I have read CONTRIBUTING.
    / 我已阅读 CONTRIBUTING
  • I confirm this contribution follows the repository license, contribution policy, and code of conduct.
    / 我确认此贡献符合仓库许可证、贡献规范和行为准则。
  • I have formatted the changed code with gofmt, go fmt, or prettier where applicable.
    / 我已按适用情况使用 gofmtgo fmtprettier 格式化变更代码。
  • I have requested review from relevant maintainers or code owners where applicable.
    / 我已在适用情况下请求相关维护者或代码所有者审查。

AI Disclosure / AI 使用声明

  • This PR includes AI-assisted content.
    / 此 PR 包含 AI 辅助内容。

Tools used / 使用工具:

  • ChatGPT
  • Codex
  • GitHub Copilot
  • Claude
  • Gemini
  • Other (please specify) / 其他(请注明):

Usage scope / 使用范围:

  • Code generation / 代码生成
  • Refactoring / 重构
  • Documentation / 文档
  • Tests / 测试
  • Translation / 翻译
  • Review assistance / 审查辅助
  • I have reviewed and validated all AI-assisted content included in this PR.
    / 我已审核并验证此 PR 中的所有 AI 辅助内容。
  • I have ensured that all AI-assisted commits include Co-Authored-By attribution.
    / 我已确保所有 AI 辅助提交都包含 Co-Authored-By 归属信息。
  • I can reproduce all AI-assisted content included in this PR without any AI tools.
    / 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 VirtualHost TypeScript interface and registered it in the type exports.
  • Created two new management pages (VirtualHosts.tsx for listing and AddOrEdit.tsx for 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.

Comment thread src/pages/manage/virtual_hosts/AddOrEdit.tsx Outdated
@PIKACHUIM PIKACHUIM changed the title feat(func): support virtual host feat(func): support virtual host merge to sharing record May 26, 2026
@PIKACHUIM PIKACHUIM requested a review from Suyunmeng May 26, 2026 05:07
@PIKACHUIM PIKACHUIM changed the title feat(func): support virtual host merge to sharing record feat(func): support virtual host via sharing record May 26, 2026
jyxjjj
jyxjjj previously approved these changes May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants