Skip to content

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

Open
PIKACHUIM wants to merge 16 commits into
mainfrom
dev-vhost
Open

feat(func): support virtual host via sharing record#2202
PIKACHUIM wants to merge 16 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 辅助内容。

@PIKACHUIM PIKACHUIM self-assigned this Mar 9, 2026
@PIKACHUIM PIKACHUIM requested review from KirCute, jyxjjj and xrgzs and removed request for xrgzs March 9, 2026 06:53
@PIKACHUIM PIKACHUIM linked an issue Mar 9, 2026 that may be closed by this pull request
8 tasks
@PIKACHUIM PIKACHUIM linked an issue Mar 9, 2026 that may be closed by this pull request
8 tasks
@PIKACHUIM PIKACHUIM requested a review from xrgzs March 9, 2026 06:57
@jyxjjj jyxjjj requested a review from Copilot March 9, 2026 07:07
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 full Virtual Host (vhost) support, enabling domain-based path remapping for the SPA and an optional “Web Hosting” mode that serves static site files (e.g., index.html) directly from a configured backend path.

Changes:

  • Add vhost CRUD APIs and persistence (model/db/op + admin routes).
  • Implement vhost-aware path remapping for FS APIs and download/preview routes, including stripping vhost prefixes when generating /p/... links.
  • Add vhost-aware static catch-all handling, including a Web Hosting file-serving path with forced Content-Type/Content-Disposition.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
server/static/static.go Adds vhost-aware catch-all handler and Web Hosting file proxying with header overrides.
server/router.go Adds /api/admin/vhost/* routes for vhost management.
server/middlewares/virtual_host.go Introduces a vhost context middleware (currently not wired).
server/middlewares/down.go Applies vhost path remapping for /d//p style routes.
server/handles/virtual_host.go Adds admin handlers for listing/getting/creating/updating/deleting vhosts.
server/handles/fsread.go Applies vhost path remapping to fs/list and fs/get, and strips vhost prefix when generating /p links.
internal/op/virtual_host.go Adds cached vhost lookup and CRUD wrappers with cache invalidation.
internal/model/virtual_host.go Adds VirtualHost model.
internal/db/virtual_host.go Adds DB layer CRUD for vhosts.
internal/db/db.go Adds VirtualHost to AutoMigrate.
internal/conf/const.go Adds context keys for vhost and vhost-prefix propagation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/static/static.go Outdated
Comment thread server/static/static.go Outdated
Comment thread server/handles/fsread.go
Comment thread server/handles/fsread.go Outdated
Comment thread server/middlewares/virtual_host.go Outdated
Comment thread internal/op/virtual_host.go Outdated
Comment thread internal/db/virtual_host.go Outdated
Comment thread server/static/static.go Outdated
Comment thread server/middlewares/down.go Outdated
Comment thread server/static/static.go Outdated
PIKACHUIM and others added 2 commits March 9, 2026 15:19
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as outdated.

PIKACHUIM and others added 2 commits March 9, 2026 21:33
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 9, 2026

@PIKACHUIM I've opened a new pull request, #2212, to work on those changes. Once the pull request is ready, I'll request review from you.

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

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/handles/fsread.go Outdated
Comment thread internal/op/virtual_host.go Outdated
Comment thread server/static/static.go Outdated
* Initial plan

* Fix vhost security and code quality issues from review feedback

Co-authored-by: PIKACHUIM <40362270+PIKACHUIM@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: PIKACHUIM <40362270+PIKACHUIM@users.noreply.github.com>
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

Copilot reviewed 12 out of 13 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/op/virtual_host.go Outdated
Comment thread server/static/static.go Outdated
Comment thread server/static/static.go
Comment thread server/handles/fsread.go Outdated
@KirCute
Copy link
Copy Markdown
Member

KirCute commented Mar 10, 2026

这个功能是不是相当于一种另类的分享,做成分享的子功能会不会好点

@PIKACHUIM
Copy link
Copy Markdown
Member Author

这个功能是不是相当于一种另类的分享,做成分享的子功能会不会好点

我认为不是,定位的是类似于Lucky或者Netpanel的网站托管
绑定域名,返回特定目录的文件列表或者文件
比如绑定:www.example.com -> 绑定到/path/path/
别人使用此域名访问,直接访问/path/path/

进一步,如果启用了静态页面托管,会直接返回/path/path/index.html

@OpenListTeam OpenListTeam deleted a comment from Copilot AI Mar 10, 2026
@KirCute
Copy link
Copy Markdown
Member

KirCute commented Mar 10, 2026

我认为不是,定位的是类似于Lucky或者Netpanel的网站托管 绑定域名,返回特定目录的文件列表或者文件 比如绑定:www.example.com -> 绑定到/path/path/ 别人使用此域名访问,直接访问/path/path/

进一步,如果启用了静态页面托管,会直接返回/path/path/index.html

我知道这个功能是网站托管,我理解的是这两个功能都是面向不特定人群,提供某个路径的公开下载服务,只不过一个访问的是/@s开头的路径,一个访问的是新域名,一个访问后得到文件列表,一个访问后直接以inline方式下载文件。那我觉得这个功能完全可以把

  • 绑定www.example.com/path/path

改设计为

  • 分享/path/path,给这个分享绑定域名www.example.com

这样这个功能就只用处理路由和响应头相关的一些问题,也不用加一张表以及与之配套的CRUD,还可以充分的利用分享的限时、限次数访问功能,由于二者权限方面的模型及其相近,也不容易写出权限方面的bug,真有这方面bug修一个等于修了俩

@PIKACHUIM
Copy link
Copy Markdown
Member Author

我认为不是,定位的是类似于Lucky或者Netpanel的网站托管 绑定域名,返回特定目录的文件列表或者文件 比如绑定:www.example.com -> 绑定到/path/path/ 别人使用此域名访问,直接访问/path/path/
进一步,如果启用了静态页面托管,会直接返回/path/path/index.html

我知道这个功能是网站托管,我理解的是这两个功能都是面向不特定人群,提供某个路径的公开下载服务,只不过一个访问的是/@s开头的路径,一个访问的是新域名,一个访问后得到文件列表,一个访问后直接以inline方式下载文件。那我觉得这个功能完全可以把

  • 绑定www.example.com/path/path

改设计为

  • 分享/path/path,给这个分享绑定域名www.example.com

这样这个功能就只用处理路由和响应头相关的一些问题,也不用加一张表以及与之配套的CRUD,还可以充分的利用分享的限时、限次数访问功能,由于二者权限方面的模型及其相近,也不容易写出权限方面的bug,真有这方面bug修一个等于修了俩

嗯,我来看下能不能合并一下

@Suyunmeng Suyunmeng force-pushed the main branch 4 times, most recently from a31fd53 to 7bea29c Compare April 2, 2026 17:27
@PIKACHUIM PIKACHUIM changed the title feat(func): support virtual host feat(func): support virtual host merge to sharing record May 26, 2026
@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
@PIKACHUIM PIKACHUIM requested a review from Suyunmeng May 26, 2026 06:13
@PIKACHUIM PIKACHUIM added the Module: Server API and protocol changes label May 26, 2026
Copy link
Copy Markdown
Member

@xrgzs xrgzs left a comment

Choose a reason for hiding this comment

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

拒绝合并

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Module: Server API and protocol changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 虚拟主机 & Web 托管

5 participants