1
0
Fork 0
mirror of https://github.com/adnanh/webhook.git synced 2026-07-21 01:15:37 +00:00

fix: 修复ip白名单机制

This commit is contained in:
jason.liao 2026-05-11 18:53:08 +08:00
parent cc338cd3ac
commit 4018195ab5
6 changed files with 151 additions and 24 deletions

View file

@ -5,30 +5,30 @@
var basePath = adminConfig.basePath || "";
var sourceOptions = [
{ value: "", label: "Select source" },
{ value: "header", label: "Header" },
{ value: "url", label: "URL Query" },
{ value: "query", label: "Query Alias" },
{ value: "payload", label: "Payload" },
{ value: "raw-request-body", label: "Raw Request Body" },
{ value: "request", label: "Request" },
{ value: "string", label: "Static String" },
{ value: "entire-payload", label: "Entire Payload" },
{ value: "entire-query", label: "Entire Query" },
{ value: "entire-headers", label: "Entire Headers" }
{ value: "", label: "Select source / 选择来源" },
{ value: "header", label: "Header / 请求头" },
{ value: "url", label: "URL Query / URL查询参数" },
{ value: "query", label: "Query Alias / 查询别名" },
{ value: "payload", label: "Payload / 请求体" },
{ value: "raw-request-body", label: "Raw Request Body / 原始请求体" },
{ value: "request", label: "Request / 请求信息" },
{ value: "string", label: "Static String / 静态字符串" },
{ value: "entire-payload", label: "Entire Payload / 完整请求体" },
{ value: "entire-query", label: "Entire Query / 完整查询参数" },
{ value: "entire-headers", label: "Entire Headers / 完整请求头" }
];
var matchTypeOptions = [
{ value: "value", label: "Value Equals" },
{ value: "regex", label: "Regex Match" },
{ value: "payload-hmac-sha1", label: "Payload HMAC SHA1" },
{ value: "payload-hmac-sha256", label: "Payload HMAC SHA256" },
{ value: "payload-hmac-sha512", label: "Payload HMAC SHA512" },
{ value: "payload-hash-sha1", label: "Payload Hash SHA1 (Deprecated)" },
{ value: "payload-hash-sha256", label: "Payload Hash SHA256 (Deprecated)" },
{ value: "payload-hash-sha512", label: "Payload Hash SHA512 (Deprecated)" },
{ value: "ip-whitelist", label: "IP Whitelist" },
{ value: "scalr-signature", label: "Scalr Signature" }
{ value: "value", label: "Value Equals / 值匹配" },
{ value: "regex", label: "Regex Match / 正则匹配" },
{ value: "payload-hmac-sha1", label: "Payload HMAC SHA1 / 签名验证" },
{ value: "payload-hmac-sha256", label: "Payload HMAC SHA256 / 签名验证" },
{ value: "payload-hmac-sha512", label: "Payload HMAC SHA512 / 签名验证" },
{ value: "payload-hash-sha1", label: "Payload Hash SHA1 (已弃用)" },
{ value: "payload-hash-sha256", label: "Payload Hash SHA256 (已弃用)" },
{ value: "payload-hash-sha512", label: "Payload Hash SHA512 (已弃用)" },
{ value: "ip-whitelist", label: "IP Whitelist / IP白名单" },
{ value: "scalr-signature", label: "Scalr Signature / Scalr签名" }
];
var state = {
@ -581,9 +581,13 @@
}
if (match.type === "ip-whitelist") {
fields.appendChild(createLabeledField("IP Range", createTextInput(match["ip-range"] || match.ipRange, "192.168.1.0/24 10.0.0.1", function (nextRange) {
fields.appendChild(createLabeledField("IP Range (IP白名单)", createTextInput(match["ip-range"] || match.ipRange, "192.168.1.0/24 10.0.0.1", function (nextRange) {
ensureMatch(rule)["ip-range"] = nextRange;
})));
var hint = document.createElement("div");
hint.className = "field-hint";
hint.textContent = "支持 CIDR 和单 IP空格分隔多个。若经过反向代理需配置 --trusted-proxies 和 --real-ip-header 才能获取真实客户端 IP。";
fields.appendChild(hint);
}
card.appendChild(fields);

View file

@ -995,7 +995,11 @@ const (
// Evaluate MatchRule will return based on the type
func (r MatchRule) Evaluate(req *Request) (bool, error) {
if r.Type == IPWhitelist {
return CheckIPWhitelist(req.RawRequest.RemoteAddr, r.IPRange)
addr := req.RealIP
if addr == "" {
addr = req.RawRequest.RemoteAddr
}
return CheckIPWhitelist(addr, r.IPRange)
}
if r.Type == ScalrSignature {
return CheckScalrSignature(req, r.Secret, true)

View file

@ -36,6 +36,11 @@ type Request struct {
// Treat signature errors as simple validate failures.
AllowSignatureErrors bool
// RealIP is the resolved client IP address. When the request comes through
// a trusted reverse proxy, this is extracted from the configured header
// (e.g. X-Real-Ip). Otherwise it equals RawRequest.RemoteAddr.
RealIP string
}
func (r *Request) ParseJSONPayload() error {

90
realip.go Normal file
View file

@ -0,0 +1,90 @@
package main
import (
"net"
"net/http"
"strings"
)
// parsedTrustedProxies holds the parsed CIDR networks from the --trusted-proxies flag.
var parsedTrustedProxies []*net.IPNet
// initTrustedProxies parses the --trusted-proxies flag value into CIDR networks.
// Must be called after flag.Parse().
func initTrustedProxies() {
if *trustedProxies == "" {
return
}
for _, entry := range strings.Split(*trustedProxies, ",") {
entry = strings.TrimSpace(entry)
if entry == "" {
continue
}
// If it's a plain IP without CIDR notation, add /32 (IPv4) or /128 (IPv6).
if !strings.Contains(entry, "/") {
if strings.Contains(entry, ":") {
entry += "/128"
} else {
entry += "/32"
}
}
_, cidr, err := net.ParseCIDR(entry)
if err != nil {
continue
}
parsedTrustedProxies = append(parsedTrustedProxies, cidr)
}
}
// isTrustedProxy checks whether the given remote address (IP:port) belongs to
// a trusted proxy network.
func isTrustedProxy(remoteAddr string) bool {
if len(parsedTrustedProxies) == 0 {
return false
}
ip := extractIP(remoteAddr)
if ip == nil {
return false
}
for _, cidr := range parsedTrustedProxies {
if cidr.Contains(ip) {
return true
}
}
return false
}
// resolveRealIP determines the real client IP address. If the request comes
// from a trusted proxy and --real-ip-header is configured, the IP is read from
// that header. Otherwise the TCP remote address is used.
func resolveRealIP(r *http.Request) string {
if *realIPHeader != "" && isTrustedProxy(r.RemoteAddr) {
headerVal := strings.TrimSpace(r.Header.Get(*realIPHeader))
if headerVal != "" {
// X-Forwarded-For may contain multiple IPs; take the first one.
if idx := strings.IndexByte(headerVal, ','); idx != -1 {
headerVal = strings.TrimSpace(headerVal[:idx])
}
return headerVal
}
}
return r.RemoteAddr
}
// extractIP parses an IP from an address string that may include a port.
func extractIP(addr string) net.IP {
addr = strings.Trim(addr, " []")
// Try host:port split first.
host, _, err := net.SplitHostPort(addr)
if err == nil {
return net.ParseIP(host)
}
return net.ParseIP(addr)
}

View file

@ -51,6 +51,8 @@ var (
maxMultipartMem = flag.Int64("max-multipart-mem", 1<<20, "maximum memory in bytes for parsing multipart form data before disk caching")
httpMethods = flag.String("http-methods", "", `set default allowed HTTP methods (ie. "POST"); separate methods with comma`)
pidPath = flag.String("pidfile", "", "create PID file at the given path")
realIPHeader = flag.String("real-ip-header", "", "header to extract real client IP from when behind a reverse proxy (e.g. X-Real-Ip)")
trustedProxies = flag.String("trusted-proxies", "", "comma-separated list of trusted proxy IPs or CIDRs; required for real-ip-header to take effect")
responseHeaders hook.ResponseHeaders
hooksFiles hook.HooksFiles
@ -110,6 +112,8 @@ func main() {
flag.Parse()
initTrustedProxies()
if err := initExecutionSettings(); err != nil {
fmt.Println("error:", err)
os.Exit(1)
@ -358,9 +362,10 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
req := &hook.Request{
ID: middleware.GetReqID(r.Context()),
RawRequest: r,
RealIP: resolveRealIP(r),
}
log.Printf("[%s] incoming HTTP %s request from %s\n", req.ID, r.Method, r.RemoteAddr)
log.Printf("[%s] incoming HTTP %s request from %s\n", req.ID, r.Method, req.RealIP)
// TODO: rename this to avoid confusion with Request.ID
id := mux.Vars(r)["id"]

View file

@ -24,6 +24,25 @@ urlprefix: wh
# 留空表示允许所有方法
# http-methods: "POST"
# ============================================================
# 反向代理 / 真实 IP
# ============================================================
# 当 webhook 部署在 Nginx 等反向代理后面时RemoteAddr 是代理的内网 IP
# 需要通过以下两个参数配合,让 ip-whitelist 规则能正确识别真实客户端 IP。
# 从哪个请求头获取真实客户端 IP
# 常见值: "X-Real-Ip"Nginx proxy_set_header X-Real-IP
# "X-Forwarded-For"(取第一个 IP
# 留空表示不启用,直接使用 RemoteAddr
# real-ip-header: "X-Real-Ip"
# 可信代理 IP 或 CIDR 列表,逗号分隔
# 只有当请求来自这些地址时,才会从 real-ip-header 中提取真实 IP
# 防止客户端伪造 header 绕过 IP 白名单
# 示例: "127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
# trusted-proxies: "127.0.0.1,10.0.0.0/8"
# ============================================================
# Hook 定义文件
# ============================================================