mirror of
https://github.com/adnanh/webhook.git
synced 2026-07-21 01:15:37 +00:00
fix: 修复ip白名单机制
This commit is contained in:
parent
cc338cd3ac
commit
4018195ab5
6 changed files with 151 additions and 24 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
90
realip.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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 定义文件
|
||||
# ============================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue