复制项目
This commit is contained in:
292
internal/api/util/api.go
Normal file
292
internal/api/util/api.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/common/mctx"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
)
|
||||
|
||||
type Api struct {
|
||||
ImUserID string
|
||||
ProxyHeader string
|
||||
ChatAdminUserID string
|
||||
}
|
||||
|
||||
func (o *Api) WithAdminUser(ctx context.Context) context.Context {
|
||||
return mctx.WithAdminUser(ctx, o.ChatAdminUserID)
|
||||
}
|
||||
|
||||
// isPrivateIP 检查IP是否是内网IP
|
||||
func isPrivateIP(ip net.IP) bool {
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
// 检查是否是内网IP范围
|
||||
// 10.0.0.0/8
|
||||
// 172.16.0.0/12
|
||||
// 192.168.0.0/16
|
||||
// 127.0.0.0/8 (localhost)
|
||||
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
|
||||
return true
|
||||
}
|
||||
ip4 := ip.To4()
|
||||
if ip4 == nil {
|
||||
return false
|
||||
}
|
||||
// 10.0.0.0/8
|
||||
if ip4[0] == 10 {
|
||||
return true
|
||||
}
|
||||
// 172.16.0.0/12
|
||||
if ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31 {
|
||||
return true
|
||||
}
|
||||
// 192.168.0.0/16
|
||||
if ip4[0] == 192 && ip4[1] == 168 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseForwardedHeader 解析标准的 Forwarded 头(RFC 7239)
|
||||
// 格式:Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43
|
||||
func parseForwardedHeader(forwarded string) string {
|
||||
// 查找 for= 后面的IP地址
|
||||
parts := strings.Split(forwarded, ";")
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if strings.HasPrefix(strings.ToLower(part), "for=") {
|
||||
ip := strings.TrimPrefix(part, "for=")
|
||||
ip = strings.TrimPrefix(ip, "For=")
|
||||
ip = strings.TrimPrefix(ip, "FOR=")
|
||||
// 移除可能的引号和端口号
|
||||
ip = strings.Trim(ip, `"`)
|
||||
if idx := strings.Index(ip, ":"); idx != -1 {
|
||||
ip = ip[:idx]
|
||||
}
|
||||
if parsedIP := net.ParseIP(ip); parsedIP != nil {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetClientIP 获取客户端真实IP地址
|
||||
// 支持多层反向代理和CDN场景
|
||||
// 优先级:
|
||||
// 1. CDN特定头(CF-Connecting-IP, True-Client-IP等)- 最可靠
|
||||
// 2. 标准 Forwarded 头(RFC 7239)- 现代标准
|
||||
// 3. X-Real-IP - 通常由第一层代理设置,比X-Forwarded-For更可靠
|
||||
// 4. X-Forwarded-For - 取最左边的IP(客户端真实IP),格式:client_ip, proxy1_ip, proxy2_ip, ...
|
||||
// 5. 其他常见头(X-Client-IP, True-Client-IP等)
|
||||
// 6. 自定义ProxyHeader
|
||||
// 7. RemoteAddr - 最后回退
|
||||
func (o *Api) GetClientIP(c *gin.Context) (string, error) {
|
||||
// 记录所有相关的HTTP头用于调试
|
||||
headers := map[string]string{
|
||||
"Forwarded": c.Request.Header.Get("Forwarded"), // RFC 7239 标准头
|
||||
"X-Forwarded-For": c.Request.Header.Get("X-Forwarded-For"),
|
||||
"X-Real-IP": c.Request.Header.Get("X-Real-IP"),
|
||||
"CF-Connecting-IP": c.Request.Header.Get("CF-Connecting-IP"), // Cloudflare - 真实客户端IP
|
||||
"CF-Ray": c.Request.Header.Get("CF-Ray"), // Cloudflare - 请求ID,用于验证来自CF
|
||||
"True-Client-IP": c.Request.Header.Get("True-Client-IP"), // Cloudflare Enterprise, Akamai
|
||||
"X-Client-IP": c.Request.Header.Get("X-Client-IP"), // 一些代理
|
||||
"X-Forwarded": c.Request.Header.Get("X-Forwarded"), // 一些代理
|
||||
"Forwarded-For": c.Request.Header.Get("Forwarded-For"), // 非标准,但有些代理使用
|
||||
"X-Cluster-Client-IP": c.Request.Header.Get("X-Cluster-Client-IP"), // Kubernetes
|
||||
"RemoteAddr": c.Request.RemoteAddr,
|
||||
}
|
||||
if o.ProxyHeader != "" {
|
||||
headers["Custom-"+o.ProxyHeader] = c.Request.Header.Get(o.ProxyHeader)
|
||||
}
|
||||
|
||||
// 打印所有能获取到的IP相关信息用于调试
|
||||
customProxyHeader := ""
|
||||
if o.ProxyHeader != "" {
|
||||
customProxyHeader = headers["Custom-"+o.ProxyHeader]
|
||||
}
|
||||
log.ZInfo(c, "GetClientIP 调试信息 - 所有HTTP头",
|
||||
"Forwarded", headers["Forwarded"],
|
||||
"X-Forwarded-For", headers["X-Forwarded-For"],
|
||||
"X-Real-IP", headers["X-Real-IP"],
|
||||
"CF-Connecting-IP", headers["CF-Connecting-IP"],
|
||||
"CF-Ray", headers["CF-Ray"],
|
||||
"True-Client-IP", headers["True-Client-IP"],
|
||||
"X-Client-IP", headers["X-Client-IP"],
|
||||
"X-Forwarded", headers["X-Forwarded"],
|
||||
"Forwarded-For", headers["Forwarded-For"],
|
||||
"X-Cluster-Client-IP", headers["X-Cluster-Client-IP"],
|
||||
"RemoteAddr", headers["RemoteAddr"],
|
||||
"Custom-ProxyHeader", customProxyHeader)
|
||||
|
||||
// 1. 优先检查 Cloudflare 的 CF-Connecting-IP(最可靠,Cloudflare 直接设置真实客户端IP)
|
||||
// 检查是否来自 Cloudflare(通过 CF-Ray 头判断)
|
||||
cfRay := c.Request.Header.Get("CF-Ray")
|
||||
cfConnectingIP := c.Request.Header.Get("CF-Connecting-IP")
|
||||
if cfConnectingIP != "" {
|
||||
// 特别记录 Cloudflare 相关信息
|
||||
log.ZInfo(c, "GetClientIP detected Cloudflare request",
|
||||
"CF-Connecting-IP", cfConnectingIP,
|
||||
"CF-Ray", cfRay)
|
||||
|
||||
ip := strings.TrimSpace(cfConnectingIP)
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP != nil {
|
||||
// Cloudflare 的 CF-Connecting-IP 总是包含真实客户端IP,无论公网还是内网
|
||||
// 如果同时有 CF-Ray,说明确实来自 Cloudflare,更加可靠
|
||||
if cfRay != "" {
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", "CF-Connecting-IP",
|
||||
"cfRay", cfRay,
|
||||
"isPrivateIP", isPrivateIP(parsedIP))
|
||||
} else {
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", "CF-Connecting-IP",
|
||||
"isPrivateIP", isPrivateIP(parsedIP))
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查其他CDN特定的头
|
||||
cdnHeaders := []string{
|
||||
"True-Client-IP", // Cloudflare Enterprise, Akamai
|
||||
"X-Client-IP", // 一些CDN和代理
|
||||
"X-Cluster-Client-IP", // Kubernetes Ingress
|
||||
}
|
||||
for _, headerName := range cdnHeaders {
|
||||
ipStr := c.Request.Header.Get(headerName)
|
||||
if ipStr != "" {
|
||||
ip := strings.TrimSpace(ipStr)
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP != nil {
|
||||
// CDN头通常包含真实客户端IP,优先返回公网IP
|
||||
if !isPrivateIP(parsedIP) {
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", headerName,
|
||||
"isPrivateIP", false)
|
||||
return ip, nil
|
||||
}
|
||||
// 即使是内网IP,CDN头也相对可靠
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", headerName,
|
||||
"isPrivateIP", true)
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查标准 Forwarded 头(RFC 7239)- 现代标准,最可靠
|
||||
forwarded := c.Request.Header.Get("Forwarded")
|
||||
if forwarded != "" {
|
||||
ip := parseForwardedHeader(forwarded)
|
||||
if ip != "" {
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP != nil {
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", "Forwarded",
|
||||
"isPrivateIP", isPrivateIP(parsedIP))
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 检查 X-Real-IP 头(通常由第一层代理设置,比X-Forwarded-For更可靠)
|
||||
xRealIP := c.Request.Header.Get("X-Real-IP")
|
||||
if xRealIP != "" {
|
||||
ip := strings.TrimSpace(xRealIP)
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP != nil {
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", "X-Real-IP",
|
||||
"isPrivateIP", isPrivateIP(parsedIP))
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 检查 X-Forwarded-For 头(格式:client_ip, proxy1_ip, proxy2_ip, ...)
|
||||
// 重要:在多层代理中,最左边的IP是客户端真实IP,应该优先取第一个IP
|
||||
xForwardedFor := c.Request.Header.Get("X-Forwarded-For")
|
||||
if xForwardedFor != "" {
|
||||
ips := strings.Split(xForwardedFor, ",")
|
||||
// 取最左边的IP(第一个IP),这是客户端真实IP
|
||||
// 格式:client_ip, proxy1_ip, proxy2_ip, ...
|
||||
if len(ips) > 0 {
|
||||
ip := strings.TrimSpace(ips[0])
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP != nil {
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", "X-Forwarded-For",
|
||||
"totalIPs", len(ips),
|
||||
"isPrivateIP", isPrivateIP(parsedIP),
|
||||
"note", "取最左边的IP(客户端真实IP)")
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 检查其他常见头
|
||||
otherHeaders := []string{"X-Client-IP", "X-Forwarded", "Forwarded-For"}
|
||||
for _, headerName := range otherHeaders {
|
||||
ipStr := c.Request.Header.Get(headerName)
|
||||
if ipStr != "" {
|
||||
ip := strings.TrimSpace(ipStr)
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP != nil {
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", headerName,
|
||||
"isPrivateIP", isPrivateIP(parsedIP))
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 如果配置了自定义的 ProxyHeader,检查它
|
||||
if o.ProxyHeader != "" {
|
||||
customHeaderIP := c.Request.Header.Get(o.ProxyHeader)
|
||||
if customHeaderIP != "" {
|
||||
ip := strings.TrimSpace(customHeaderIP)
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP != nil {
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", "Custom-"+o.ProxyHeader,
|
||||
"isPrivateIP", isPrivateIP(parsedIP))
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 最后使用 RemoteAddr(在集群环境中可能是代理服务器的IP)
|
||||
remoteAddr := c.Request.RemoteAddr
|
||||
ip, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
log.ZError(c, "GetClientIP failed to parse RemoteAddr", err, "RemoteAddr", remoteAddr)
|
||||
return "", errs.ErrInternalServer.WrapMsg(fmt.Sprintf("failed to parse RemoteAddr: %v", err))
|
||||
}
|
||||
parsedIP := net.ParseIP(ip)
|
||||
log.ZInfo(c, "GetClientIP 最终选择IP",
|
||||
"ip", ip,
|
||||
"source", "RemoteAddr",
|
||||
"isPrivateIP", parsedIP != nil && isPrivateIP(parsedIP))
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func (o *Api) GetDefaultIMAdminUserID() string {
|
||||
return o.ImUserID
|
||||
}
|
||||
Reference in New Issue
Block a user