Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/conf/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,4 +191,6 @@ const (
PathKey
SharingIDKey
SkipHookKey
VirtualHostKey
VhostPrefixKey
)
2 changes: 1 addition & 1 deletion internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ var db *gorm.DB

func Init(d *gorm.DB) {
db = d
err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey), new(model.SharingDB))
err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey), new(model.SharingDB), new(model.VirtualHost))
if err != nil {
log.Fatalf("failed migrate database: %s", err.Error())
}
Expand Down
45 changes: 45 additions & 0 deletions internal/db/virtual_host.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package db

import (
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/pkg/errors"
)

func GetVirtualHostByDomain(domain string) (*model.VirtualHost, error) {
var v model.VirtualHost
if err := db.Where("domain = ?", domain).First(&v).Error; err != nil {
return nil, errors.Wrapf(err, "failed to select virtual host")
}
return &v, nil
}

func GetVirtualHostById(id uint) (*model.VirtualHost, error) {
var v model.VirtualHost
if err := db.First(&v, id).Error; err != nil {
return nil, errors.Wrapf(err, "failed get virtual host")
}
return &v, nil
}

func CreateVirtualHost(v *model.VirtualHost) error {
return errors.WithStack(db.Create(v).Error)
}

func UpdateVirtualHost(v *model.VirtualHost) error {
return errors.WithStack(db.Save(v).Error)
}

func GetVirtualHosts(pageIndex, pageSize int) (vhosts []model.VirtualHost, count int64, err error) {
vhostDB := db.Model(&model.VirtualHost{})
if err = vhostDB.Count(&count).Error; err != nil {
return nil, 0, errors.Wrapf(err, "failed get virtual hosts count")
}
if err = vhostDB.Order(columnName("id")).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&vhosts).Error; err != nil {
return nil, 0, errors.Wrapf(err, "failed find virtual hosts")
}
return vhosts, count, nil
}

func DeleteVirtualHostById(id uint) error {
return errors.WithStack(db.Delete(&model.VirtualHost{}, id).Error)
}
9 changes: 9 additions & 0 deletions internal/model/virtual_host.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package model

type VirtualHost struct {
ID uint `json:"id" gorm:"primaryKey"`
Enabled bool `json:"enabled"`
Domain string `json:"domain" gorm:"unique" binding:"required"`
Path string `json:"path" binding:"required"`
WebHosting bool `json:"web_hosting"`
}
75 changes: 75 additions & 0 deletions internal/op/virtual_host.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package op

import (
"time"

"github.com/OpenListTeam/OpenList/v4/internal/db"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/go-cache"
"github.com/pkg/errors"
"gorm.io/gorm"
)

var vhostCache = cache.NewMemCache(cache.WithShards[*model.VirtualHost](2))

// GetVirtualHostByDomain 根据域名获取虚拟主机配置(带缓存)
func GetVirtualHostByDomain(domain string) (*model.VirtualHost, error) {
if v, ok := vhostCache.Get(domain); ok {
if v == nil {
utils.Log.Debugf("[VirtualHost] cache hit (nil) for domain=%q", domain)
return nil, errors.New("virtual host not found")
}
utils.Log.Debugf("[VirtualHost] cache hit for domain=%q id=%d", domain, v.ID)
return v, nil
}
utils.Log.Debugf("[VirtualHost] cache miss for domain=%q, querying db...", domain)
v, err := db.GetVirtualHostByDomain(domain)
if err != nil {
Comment thread
PIKACHUIM marked this conversation as resolved.
Outdated
if errors.Is(errors.Cause(err), gorm.ErrRecordNotFound) {
utils.Log.Debugf("[VirtualHost] domain=%q not found in db, caching nil", domain)
vhostCache.Set(domain, nil, cache.WithEx[*model.VirtualHost](time.Minute*5))
return nil, errors.New("virtual host not found")
}
utils.Log.Errorf("[VirtualHost] db error for domain=%q: %v", domain, err)
return nil, err
}
utils.Log.Debugf("[VirtualHost] db found domain=%q id=%d enabled=%v web_hosting=%v", domain, v.ID, v.Enabled, v.WebHosting)
vhostCache.Set(domain, v, cache.WithEx[*model.VirtualHost](time.Hour))
return v, nil
}

func GetVirtualHostById(id uint) (*model.VirtualHost, error) {
return db.GetVirtualHostById(id)
}

func CreateVirtualHost(v *model.VirtualHost) error {
v.Path = utils.FixAndCleanPath(v.Path)
vhostCache.Del(v.Domain)
return db.CreateVirtualHost(v)
}

func UpdateVirtualHost(v *model.VirtualHost) error {
v.Path = utils.FixAndCleanPath(v.Path)
old, err := db.GetVirtualHostById(v.ID)
if err != nil {
return err
}
// 如果域名变更,清除旧域名缓存
vhostCache.Del(old.Domain)
vhostCache.Del(v.Domain)
return db.UpdateVirtualHost(v)
Comment thread
PIKACHUIM marked this conversation as resolved.
Outdated
}

func DeleteVirtualHostById(id uint) error {
old, err := db.GetVirtualHostById(id)
if err != nil {
return err
}
vhostCache.Del(old.Domain)
return db.DeleteVirtualHostById(id)
}

func GetVirtualHosts(pageIndex, pageSize int) ([]model.VirtualHost, int64, error) {
return db.GetVirtualHosts(pageIndex, pageSize)
}
68 changes: 67 additions & 1 deletion server/handles/fsread.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ func FsListSplit(c *gin.Context) {
SharingList(c, &req)
return
}
// 虚拟主机路径重映射:根据 Host 头匹配虚拟主机规则,将请求路径映射到实际路径
req.Path = applyVhostPathMapping(c, req.Path)
user := c.Request.Context().Value(conf.UserKey).(*model.User)
if user.IsGuest() && user.Disabled {
common.ErrorStrResp(c, "Guest user is disabled, login please", 401)
Expand Down Expand Up @@ -273,6 +275,11 @@ func FsGetSplit(c *gin.Context) {
SharingGet(c, &req)
return
}
// 虚拟主机路径重映射:根据 Host 头匹配虚拟主机规则,将请求路径映射到实际路径
// 同时将 vhost.Path 前缀存入 context,供 FsGet 生成 /p/ 链接时去掉前缀
var vhostPrefix string
req.Path, vhostPrefix = applyVhostPathMappingWithPrefix(c, req.Path)
common.GinWithValue(c, conf.VhostPrefixKey, vhostPrefix)
user := c.Request.Context().Value(conf.UserKey).(*model.User)
if user.IsGuest() && user.Disabled {
common.ErrorStrResp(c, "Guest user is disabled, login please", 401)
Expand Down Expand Up @@ -322,12 +329,14 @@ func FsGet(c *gin.Context, req *FsGetReq, user *model.User) {
rawURL = common.GenerateDownProxyURL(storage.GetStorage(), reqPath)
if rawURL == "" {
query := ""
// 生成 /p/ 链接时,去掉 vhost 路径前缀,保持前端看到的路径一致
downPath := stripVhostPrefix(c, reqPath)
if isEncrypt(meta, reqPath) || setting.GetBool(conf.SignAll) {
query = "?sign=" + sign.Sign(reqPath)
}
rawURL = fmt.Sprintf("%s/p%s%s",
common.GetApiUrl(c),
utils.EncodePath(reqPath, true),
utils.EncodePath(downPath, true),
query)
}
} else {
Expand Down Expand Up @@ -432,3 +441,60 @@ func FsOther(c *gin.Context) {
}
common.SuccessResp(c, res)
}

// applyVhostPathMapping 根据请求的 Host 头匹配虚拟主机规则,将请求路径映射到实际路径。
func applyVhostPathMapping(c *gin.Context, reqPath string) string {
mapped, _ := applyVhostPathMappingWithPrefix(c, reqPath)
return mapped
}

// applyVhostPathMappingWithPrefix 根据请求的 Host 头匹配虚拟主机规则,
// 将请求路径映射到虚拟主机配置的实际路径,同时返回 vhost.Path 前缀(用于生成下载链接时去掉前缀)。
// 例如:vhost.Path="/123pan/Downloads",reqPath="/",则返回 ("/123pan/Downloads", "/123pan/Downloads")
// 例如:vhost.Path="/123pan/Downloads",reqPath="/subdir",则返回 ("/123pan/Downloads/subdir", "/123pan/Downloads")
// 如果没有匹配的虚拟主机规则,则返回 (原始路径, "")
func applyVhostPathMappingWithPrefix(c *gin.Context, reqPath string) (string, string) {
rawHost := c.Request.Host
domain := stripHostPortForVhost(rawHost)
if domain == "" {
return reqPath, ""
}
vhost, err := op.GetVirtualHostByDomain(domain)
if err != nil || vhost == nil {
return reqPath, ""
}
if !vhost.Enabled || vhost.WebHosting {
// 未启用,或者是 Web 托管模式(Web 托管不做路径重映射)
return reqPath, ""
}
// 路径重映射:将 reqPath 拼接到 vhost.Path 后面
mapped := stdpath.Join(vhost.Path, reqPath)
utils.Log.Debugf("[VirtualHost] API path remapping: domain=%q reqPath=%q -> mappedPath=%q", domain, reqPath, mapped)
Comment thread
PIKACHUIM marked this conversation as resolved.
Outdated
return mapped, vhost.Path
}

Comment thread
PIKACHUIM marked this conversation as resolved.
// stripVhostPrefix 从 gin context 中取出 vhost 路径前缀,并从 path 中去掉该前缀。
// 用于生成 /p/ 下载链接时,将真实路径还原为前端看到的路径。
func stripVhostPrefix(c *gin.Context, path string) string {
prefix, ok := c.Request.Context().Value(conf.VhostPrefixKey).(string)
if !ok || prefix == "" {
return path
}
if strings.HasPrefix(path, prefix+"/") {
return path[len(prefix):]
}
if path == prefix {
return "/"
}
return path
}

// stripHostPortForVhost 去掉 host 中的端口号,返回纯域名
func stripHostPortForVhost(host string) string {
if idx := strings.LastIndex(host, ":"); idx != -1 {
if !strings.Contains(host, "[") {
return host[:idx]
}
}
return host
Comment thread
PIKACHUIM marked this conversation as resolved.
Outdated
}
83 changes: 83 additions & 0 deletions server/handles/virtual_host.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package handles

import (
"strconv"

"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/server/common"
"github.com/gin-gonic/gin"
)

func ListVirtualHosts(c *gin.Context) {
var req model.PageReq
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
req.Validate()
vhosts, total, err := op.GetVirtualHosts(req.Page, req.PerPage)
if err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c, common.PageResp{
Content: vhosts,
Total: total,
})
}

func GetVirtualHost(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.Atoi(idStr)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
vhost, err := op.GetVirtualHostById(uint(id))
if err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c, vhost)
}

func CreateVirtualHost(c *gin.Context) {
var req model.VirtualHost
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
if err := op.CreateVirtualHost(&req); err != nil {
common.ErrorResp(c, err, 500, true)
} else {
common.SuccessResp(c)
}
}

func UpdateVirtualHost(c *gin.Context) {
var req model.VirtualHost
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
if err := op.UpdateVirtualHost(&req); err != nil {
common.ErrorResp(c, err, 500, true)
} else {
common.SuccessResp(c)
}
}

func DeleteVirtualHost(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.Atoi(idStr)
if err != nil {
common.ErrorResp(c, err, 400)
return
}
if err := op.DeleteVirtualHostById(uint(id)); err != nil {
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c)
}
37 changes: 37 additions & 0 deletions server/middlewares/down.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package middlewares

import (
stdpath "path"
"strings"

"github.com/OpenListTeam/OpenList/v4/internal/conf"
Expand All @@ -17,10 +18,46 @@ import (

func PathParse(c *gin.Context) {
rawPath := parsePath(c.Param("path"))
// 虚拟主机路径重映射:根据 Host 头匹配虚拟主机规则,将请求路径映射到实际路径
// 例如:vhost.Path="/123pan/Downloads",rawPath="/tests.html" -> "/123pan/Downloads/tests.html"
rawPath = applyDownVhostPathMapping(c, rawPath)
common.GinWithValue(c, conf.PathKey, rawPath)
c.Next()
}

// applyDownVhostPathMapping 根据请求的 Host 头匹配虚拟主机规则,
// 将下载/预览路由的路径映射到虚拟主机配置的实际路径。
// 仅在虚拟主机启用且非 Web 托管模式时生效。
func applyDownVhostPathMapping(c *gin.Context, reqPath string) string {
rawHost := c.Request.Host
domain := stripDownHostPort(rawHost)
if domain == "" {
return reqPath
}
vhost, err := op.GetVirtualHostByDomain(domain)
if err != nil || vhost == nil {
return reqPath
}
if !vhost.Enabled || vhost.WebHosting {
// 未启用,或者是 Web 托管模式(Web 托管不做路径重映射)
return reqPath
}
// 路径重映射:将 reqPath 拼接到 vhost.Path 后面
mapped := stdpath.Join(vhost.Path, reqPath)
utils.Log.Debugf("[VirtualHost] down path remapping: domain=%q reqPath=%q -> mappedPath=%q", domain, reqPath, mapped)
return mapped
}

// stripDownHostPort 去掉 host 中的端口号,返回纯域名
func stripDownHostPort(host string) string {
if idx := strings.LastIndex(host, ":"); idx != -1 {
if !strings.Contains(host, "[") {
return host[:idx]
}
}
return host
Comment thread
PIKACHUIM marked this conversation as resolved.
Outdated
}

func Down(verifyFunc func(string, string) error) func(c *gin.Context) {
return func(c *gin.Context) {
rawPath := c.Request.Context().Value(conf.PathKey).(string)
Expand Down
4 changes: 4 additions & 0 deletions server/middlewares/virtual_host.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package middlewares

// Note: Virtual host resolution is handled by existing handlers/middlewares.
// This file intentionally contains no additional code to avoid unused/dead middleware.
Loading
Loading