-
-
Notifications
You must be signed in to change notification settings - Fork 2k
feat(func): support virtual host #2202
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
base: main
Are you sure you want to change the base?
Changes from all commits
12bd5a0
8a878fa
38f761f
0b54be2
a3b1029
166af74
d825769
8489950
bedf0a9
ae00551
a5f92bd
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 |
|---|---|---|
|
|
@@ -191,4 +191,5 @@ const ( | |
| PathKey | ||
| SharingIDKey | ||
| SkipHookKey | ||
| VhostPrefixKey | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| package db |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| package model |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| package op |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ package handles | |
|
|
||
| import ( | ||
| "fmt" | ||
| "net" | ||
| stdpath "path" | ||
| "strings" | ||
| "time" | ||
|
|
@@ -69,6 +70,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) | ||
|
|
@@ -272,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) | ||
|
|
@@ -319,12 +327,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 { | ||
|
|
@@ -427,3 +437,70 @@ 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 头匹配 sharing 中带 Domain 的虚拟主机记录, | ||
| // 将请求路径映射到 sharing.Files[0] 之下,同时返回该路径前缀(用于生成下载链接时去掉前缀)。 | ||
| // 例如:sharing.Files[0]="/123pan/Downloads",reqPath="/",则返回 ("/123pan/Downloads", "/123pan/Downloads") | ||
| // 例如:sharing.Files[0]="/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, "" | ||
| } | ||
| sharing, err := op.GetSharingByDomain(domain) | ||
| if err != nil || sharing == nil { | ||
| return reqPath, "" | ||
| } | ||
| if sharing.WebHosting { | ||
| // Web 托管模式不做 API 路径重映射 | ||
| return reqPath, "" | ||
| } | ||
| if len(sharing.Files) == 0 { | ||
| return reqPath, "" | ||
| } | ||
| root := sharing.Files[0] | ||
| // Map request path into the sharing root and verify it does not escape via traversal. | ||
| // stdpath.Join calls Clean internally, which collapses ".." segments, so we only need | ||
| // to confirm the result still lives under root. | ||
| mapped := stdpath.Join(root, reqPath) | ||
| if !strings.HasPrefix(mapped, strings.TrimRight(root, "/")+"/") && mapped != root { | ||
| utils.Log.Warnf("[VirtualHost] path traversal rejected for API remapping: domain=%q reqPath=%q", domain, reqPath) | ||
| return reqPath, "" | ||
| } | ||
| utils.Log.Debugf("[VirtualHost] API path remapping: domain=%q reqPath=%q -> mappedPath=%q", domain, reqPath, mapped) | ||
| return mapped, root | ||
| } | ||
|
|
||
| // 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 removes the port from a host string (supports IPv4, IPv6, and bracketed IPv6). | ||
| func stripHostPortForVhost(host string) string { | ||
| h, _, err := net.SplitHostPort(host) | ||
| if err != nil { | ||
| // No port present; return host as-is | ||
| return host | ||
| } | ||
| return h | ||
|
Comment on lines
+498
to
+505
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| package handles |
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.
applyVhostPathMappingWithPrefixusespath.Join(vhost.Path, reqPath), which cleans the input and can remove..segments beforeuser.JoinPathruns. This can bypassutils.JoinBasePath's relative-path detection (it checks for..in the original request path) and potentially allow traversal outside the user's base path. Preserve the original..check (e.g. by usingutils.JoinBasePath-style validation forreqPath) before joining, or return an error on relative paths.