mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 03:18:24 +00:00 
			
		
		
		
	Only allow webhook to send requests to allowed hosts (#17482)
This commit is contained in:
		| @@ -194,6 +194,10 @@ func listen(m http.Handler, handleRedirector bool) error { | |||||||
| 		listenAddr = net.JoinHostPort(listenAddr, setting.HTTPPort) | 		listenAddr = net.JoinHostPort(listenAddr, setting.HTTPPort) | ||||||
| 	} | 	} | ||||||
| 	log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubURL) | 	log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubURL) | ||||||
|  | 	// This can be useful for users, many users do wrong to their config and get strange behaviors behind a reverse-proxy. | ||||||
|  | 	// A user may fix the configuration mistake when he sees this log. | ||||||
|  | 	// And this is also very helpful to maintainers to provide help to users to resolve their configuration problems. | ||||||
|  | 	log.Info("AppURL(ROOT_URL): %s", setting.AppURL) | ||||||
|  |  | ||||||
| 	if setting.LFS.StartServer { | 	if setting.LFS.StartServer { | ||||||
| 		log.Info("LFS server enabled") | 		log.Info("LFS server enabled") | ||||||
|   | |||||||
| @@ -1396,6 +1396,12 @@ PATH = | |||||||
| ;; Deliver timeout in seconds | ;; Deliver timeout in seconds | ||||||
| ;DELIVER_TIMEOUT = 5 | ;DELIVER_TIMEOUT = 5 | ||||||
| ;; | ;; | ||||||
|  | ;; Webhook can only call allowed hosts for security reasons. Comma separated list, eg: external, 192.168.1.0/24, *.mydomain.com | ||||||
|  | ;; Built-in: loopback (for localhost), private (for LAN/intranet), external (for public hosts on internet), * (for all hosts) | ||||||
|  | ;; CIDR list: 1.2.3.0/8, 2001:db8::/32 | ||||||
|  | ;; Wildcard hosts: *.mydomain.com, 192.168.100.* | ||||||
|  | ;ALLOWED_HOST_LIST = external | ||||||
|  | ;; | ||||||
| ;; Allow insecure certification | ;; Allow insecure certification | ||||||
| ;SKIP_TLS_VERIFY = false | ;SKIP_TLS_VERIFY = false | ||||||
| ;; | ;; | ||||||
|   | |||||||
| @@ -581,6 +581,14 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type | |||||||
|  |  | ||||||
| - `QUEUE_LENGTH`: **1000**: Hook task queue length. Use caution when editing this value. | - `QUEUE_LENGTH`: **1000**: Hook task queue length. Use caution when editing this value. | ||||||
| - `DELIVER_TIMEOUT`: **5**: Delivery timeout (sec) for shooting webhooks. | - `DELIVER_TIMEOUT`: **5**: Delivery timeout (sec) for shooting webhooks. | ||||||
|  | - `ALLOWED_HOST_LIST`: **external**: Webhook can only call allowed hosts for security reasons. Comma separated list. | ||||||
|  |   - Built-in networks: | ||||||
|  |     - `loopback`: 127.0.0.0/8 for IPv4 and ::1/128 for IPv6, localhost is included. | ||||||
|  |     - `private`: RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) and RFC 4193 (FC00::/7). Also called LAN/Intranet. | ||||||
|  |     - `external`: A valid non-private unicast IP, you can access all hosts on public internet.  | ||||||
|  |     - `*`: All hosts are allowed. | ||||||
|  |   - CIDR list: `1.2.3.0/8` for IPv4 and `2001:db8::/32` for IPv6 | ||||||
|  |   - Wildcard hosts: `*.mydomain.com`, `192.168.100.*` | ||||||
| - `SKIP_TLS_VERIFY`: **false**: Allow insecure certification. | - `SKIP_TLS_VERIFY`: **false**: Allow insecure certification. | ||||||
| - `PAGING_NUM`: **10**: Number of webhook history events that are shown in one page. | - `PAGING_NUM`: **10**: Number of webhook history events that are shown in one page. | ||||||
| - `PROXY_URL`: **\<empty\>**: Proxy server URL, support http://, https//, socks://, blank will follow environment http_proxy/https_proxy. If not given, will use global proxy setting. | - `PROXY_URL`: **\<empty\>**: Proxy server URL, support http://, https//, socks://, blank will follow environment http_proxy/https_proxy. If not given, will use global proxy setting. | ||||||
|   | |||||||
							
								
								
									
										94
									
								
								modules/hostmatcher/hostmatcher.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								modules/hostmatcher/hostmatcher.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | |||||||
|  | // Copyright 2021 The Gitea Authors. All rights reserved. | ||||||
|  | // Use of this source code is governed by a MIT-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  |  | ||||||
|  | package hostmatcher | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/util" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // HostMatchList is used to check if a host or IP is in a list. | ||||||
|  | // If you only need to do wildcard matching, consider to use modules/matchlist | ||||||
|  | type HostMatchList struct { | ||||||
|  | 	hosts  []string | ||||||
|  | 	ipNets []*net.IPNet | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MatchBuiltinAll all hosts are matched | ||||||
|  | const MatchBuiltinAll = "*" | ||||||
|  |  | ||||||
|  | // MatchBuiltinExternal A valid non-private unicast IP, all hosts on public internet are matched | ||||||
|  | const MatchBuiltinExternal = "external" | ||||||
|  |  | ||||||
|  | // MatchBuiltinPrivate RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) and RFC 4193 (FC00::/7). Also called LAN/Intranet. | ||||||
|  | const MatchBuiltinPrivate = "private" | ||||||
|  |  | ||||||
|  | // MatchBuiltinLoopback 127.0.0.0/8 for IPv4 and ::1/128 for IPv6, localhost is included. | ||||||
|  | const MatchBuiltinLoopback = "loopback" | ||||||
|  |  | ||||||
|  | // ParseHostMatchList parses the host list HostMatchList | ||||||
|  | func ParseHostMatchList(hostList string) *HostMatchList { | ||||||
|  | 	hl := &HostMatchList{} | ||||||
|  | 	for _, s := range strings.Split(hostList, ",") { | ||||||
|  | 		s = strings.ToLower(strings.TrimSpace(s)) | ||||||
|  | 		if s == "" { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		_, ipNet, err := net.ParseCIDR(s) | ||||||
|  | 		if err == nil { | ||||||
|  | 			hl.ipNets = append(hl.ipNets, ipNet) | ||||||
|  | 		} else { | ||||||
|  | 			hl.hosts = append(hl.hosts, s) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return hl | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MatchesHostOrIP checks if the host or IP matches an allow/deny(block) list | ||||||
|  | func (hl *HostMatchList) MatchesHostOrIP(host string, ip net.IP) bool { | ||||||
|  | 	var matched bool | ||||||
|  | 	host = strings.ToLower(host) | ||||||
|  | 	ipStr := ip.String() | ||||||
|  | loop: | ||||||
|  | 	for _, hostInList := range hl.hosts { | ||||||
|  | 		switch hostInList { | ||||||
|  | 		case "": | ||||||
|  | 			continue | ||||||
|  | 		case MatchBuiltinAll: | ||||||
|  | 			matched = true | ||||||
|  | 			break loop | ||||||
|  | 		case MatchBuiltinExternal: | ||||||
|  | 			if matched = ip.IsGlobalUnicast() && !util.IsIPPrivate(ip); matched { | ||||||
|  | 				break loop | ||||||
|  | 			} | ||||||
|  | 		case MatchBuiltinPrivate: | ||||||
|  | 			if matched = util.IsIPPrivate(ip); matched { | ||||||
|  | 				break loop | ||||||
|  | 			} | ||||||
|  | 		case MatchBuiltinLoopback: | ||||||
|  | 			if matched = ip.IsLoopback(); matched { | ||||||
|  | 				break loop | ||||||
|  | 			} | ||||||
|  | 		default: | ||||||
|  | 			if matched, _ = filepath.Match(hostInList, host); matched { | ||||||
|  | 				break loop | ||||||
|  | 			} | ||||||
|  | 			if matched, _ = filepath.Match(hostInList, ipStr); matched { | ||||||
|  | 				break loop | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if !matched { | ||||||
|  | 		for _, ipNet := range hl.ipNets { | ||||||
|  | 			if matched = ipNet.Contains(ip); matched { | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return matched | ||||||
|  | } | ||||||
							
								
								
									
										119
									
								
								modules/hostmatcher/hostmatcher_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								modules/hostmatcher/hostmatcher_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | |||||||
|  | // Copyright 2021 The Gitea Authors. All rights reserved. | ||||||
|  | // Use of this source code is governed by a MIT-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  |  | ||||||
|  | package hostmatcher | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestHostOrIPMatchesList(t *testing.T) { | ||||||
|  | 	type tc struct { | ||||||
|  | 		host     string | ||||||
|  | 		ip       net.IP | ||||||
|  | 		expected bool | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// for IPv6: "::1" is loopback, "fd00::/8" is private | ||||||
|  |  | ||||||
|  | 	hl := ParseHostMatchList("private, External, *.myDomain.com, 169.254.1.0/24") | ||||||
|  | 	cases := []tc{ | ||||||
|  | 		{"", net.IPv4zero, false}, | ||||||
|  | 		{"", net.IPv6zero, false}, | ||||||
|  |  | ||||||
|  | 		{"", net.ParseIP("127.0.0.1"), false}, | ||||||
|  | 		{"", net.ParseIP("::1"), false}, | ||||||
|  |  | ||||||
|  | 		{"", net.ParseIP("10.0.1.1"), true}, | ||||||
|  | 		{"", net.ParseIP("192.168.1.1"), true}, | ||||||
|  | 		{"", net.ParseIP("fd00::1"), true}, | ||||||
|  |  | ||||||
|  | 		{"", net.ParseIP("8.8.8.8"), true}, | ||||||
|  | 		{"", net.ParseIP("1001::1"), true}, | ||||||
|  |  | ||||||
|  | 		{"mydomain.com", net.IPv4zero, false}, | ||||||
|  | 		{"sub.mydomain.com", net.IPv4zero, true}, | ||||||
|  |  | ||||||
|  | 		{"", net.ParseIP("169.254.1.1"), true}, | ||||||
|  | 		{"", net.ParseIP("169.254.2.2"), false}, | ||||||
|  | 	} | ||||||
|  | 	for _, c := range cases { | ||||||
|  | 		assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	hl = ParseHostMatchList("loopback") | ||||||
|  | 	cases = []tc{ | ||||||
|  | 		{"", net.IPv4zero, false}, | ||||||
|  | 		{"", net.ParseIP("127.0.0.1"), true}, | ||||||
|  | 		{"", net.ParseIP("10.0.1.1"), false}, | ||||||
|  | 		{"", net.ParseIP("192.168.1.1"), false}, | ||||||
|  | 		{"", net.ParseIP("8.8.8.8"), false}, | ||||||
|  |  | ||||||
|  | 		{"", net.ParseIP("::1"), true}, | ||||||
|  | 		{"", net.ParseIP("fd00::1"), false}, | ||||||
|  | 		{"", net.ParseIP("1000::1"), false}, | ||||||
|  |  | ||||||
|  | 		{"mydomain.com", net.IPv4zero, false}, | ||||||
|  | 	} | ||||||
|  | 	for _, c := range cases { | ||||||
|  | 		assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	hl = ParseHostMatchList("private") | ||||||
|  | 	cases = []tc{ | ||||||
|  | 		{"", net.IPv4zero, false}, | ||||||
|  | 		{"", net.ParseIP("127.0.0.1"), false}, | ||||||
|  | 		{"", net.ParseIP("10.0.1.1"), true}, | ||||||
|  | 		{"", net.ParseIP("192.168.1.1"), true}, | ||||||
|  | 		{"", net.ParseIP("8.8.8.8"), false}, | ||||||
|  |  | ||||||
|  | 		{"", net.ParseIP("::1"), false}, | ||||||
|  | 		{"", net.ParseIP("fd00::1"), true}, | ||||||
|  | 		{"", net.ParseIP("1000::1"), false}, | ||||||
|  |  | ||||||
|  | 		{"mydomain.com", net.IPv4zero, false}, | ||||||
|  | 	} | ||||||
|  | 	for _, c := range cases { | ||||||
|  | 		assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	hl = ParseHostMatchList("external") | ||||||
|  | 	cases = []tc{ | ||||||
|  | 		{"", net.IPv4zero, false}, | ||||||
|  | 		{"", net.ParseIP("127.0.0.1"), false}, | ||||||
|  | 		{"", net.ParseIP("10.0.1.1"), false}, | ||||||
|  | 		{"", net.ParseIP("192.168.1.1"), false}, | ||||||
|  | 		{"", net.ParseIP("8.8.8.8"), true}, | ||||||
|  |  | ||||||
|  | 		{"", net.ParseIP("::1"), false}, | ||||||
|  | 		{"", net.ParseIP("fd00::1"), false}, | ||||||
|  | 		{"", net.ParseIP("1000::1"), true}, | ||||||
|  |  | ||||||
|  | 		{"mydomain.com", net.IPv4zero, false}, | ||||||
|  | 	} | ||||||
|  | 	for _, c := range cases { | ||||||
|  | 		assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	hl = ParseHostMatchList("*") | ||||||
|  | 	cases = []tc{ | ||||||
|  | 		{"", net.IPv4zero, true}, | ||||||
|  | 		{"", net.ParseIP("127.0.0.1"), true}, | ||||||
|  | 		{"", net.ParseIP("10.0.1.1"), true}, | ||||||
|  | 		{"", net.ParseIP("192.168.1.1"), true}, | ||||||
|  | 		{"", net.ParseIP("8.8.8.8"), true}, | ||||||
|  |  | ||||||
|  | 		{"", net.ParseIP("::1"), true}, | ||||||
|  | 		{"", net.ParseIP("fd00::1"), true}, | ||||||
|  | 		{"", net.ParseIP("1000::1"), true}, | ||||||
|  |  | ||||||
|  | 		{"mydomain.com", net.IPv4zero, true}, | ||||||
|  | 	} | ||||||
|  | 	for _, c := range cases { | ||||||
|  | 		assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -89,7 +89,7 @@ func IsMigrateURLAllowed(remoteURL string, doer *models.User) error { | |||||||
| 			return &models.ErrInvalidCloneAddr{Host: u.Host, NotResolvedIP: true} | 			return &models.ErrInvalidCloneAddr{Host: u.Host, NotResolvedIP: true} | ||||||
| 		} | 		} | ||||||
| 		for _, addr := range addrList { | 		for _, addr := range addrList { | ||||||
| 			if isIPPrivate(addr) || !addr.IsGlobalUnicast() { | 			if util.IsIPPrivate(addr) || !addr.IsGlobalUnicast() { | ||||||
| 				return &models.ErrInvalidCloneAddr{Host: u.Host, PrivateNet: addr.String(), IsPermissionDenied: true} | 				return &models.ErrInvalidCloneAddr{Host: u.Host, PrivateNet: addr.String(), IsPermissionDenied: true} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @@ -474,13 +474,3 @@ func Init() error { | |||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // TODO: replace with `ip.IsPrivate()` if min go version is bumped to 1.17 |  | ||||||
| func isIPPrivate(ip net.IP) bool { |  | ||||||
| 	if ip4 := ip.To4(); ip4 != nil { |  | ||||||
| 		return ip4[0] == 10 || |  | ||||||
| 			(ip4[0] == 172 && ip4[1]&0xf0 == 16) || |  | ||||||
| 			(ip4[0] == 192 && ip4[1] == 168) |  | ||||||
| 	} |  | ||||||
| 	return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -7,20 +7,22 @@ package setting | |||||||
| import ( | import ( | ||||||
| 	"net/url" | 	"net/url" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/hostmatcher" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	// Webhook settings | 	// Webhook settings | ||||||
| 	Webhook = struct { | 	Webhook = struct { | ||||||
| 		QueueLength    int | 		QueueLength     int | ||||||
| 		DeliverTimeout int | 		DeliverTimeout  int | ||||||
| 		SkipTLSVerify  bool | 		SkipTLSVerify   bool | ||||||
| 		Types          []string | 		AllowedHostList *hostmatcher.HostMatchList | ||||||
| 		PagingNum      int | 		Types           []string | ||||||
| 		ProxyURL       string | 		PagingNum       int | ||||||
| 		ProxyURLFixed  *url.URL | 		ProxyURL        string | ||||||
| 		ProxyHosts     []string | 		ProxyURLFixed   *url.URL | ||||||
|  | 		ProxyHosts      []string | ||||||
| 	}{ | 	}{ | ||||||
| 		QueueLength:    1000, | 		QueueLength:    1000, | ||||||
| 		DeliverTimeout: 5, | 		DeliverTimeout: 5, | ||||||
| @@ -36,6 +38,7 @@ func newWebhookService() { | |||||||
| 	Webhook.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000) | 	Webhook.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000) | ||||||
| 	Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5) | 	Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5) | ||||||
| 	Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool() | 	Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool() | ||||||
|  | 	Webhook.AllowedHostList = hostmatcher.ParseHostMatchList(sec.Key("ALLOWED_HOST_LIST").MustString(hostmatcher.MatchBuiltinExternal)) | ||||||
| 	Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "matrix", "wechatwork"} | 	Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "matrix", "wechatwork"} | ||||||
| 	Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10) | 	Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10) | ||||||
| 	Webhook.ProxyURL = sec.Key("PROXY_URL").MustString("") | 	Webhook.ProxyURL = sec.Key("PROXY_URL").MustString("") | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								modules/util/net.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								modules/util/net.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | // Copyright 2021 The Gitea Authors. All rights reserved. | ||||||
|  | // Use of this source code is governed by a MIT-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  |  | ||||||
|  | package util | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // IsIPPrivate for net.IP.IsPrivate. TODO: replace with `ip.IsPrivate()` if min go version is bumped to 1.17 | ||||||
|  | func IsIPPrivate(ip net.IP) bool { | ||||||
|  | 	if ip4 := ip.To4(); ip4 != nil { | ||||||
|  | 		return ip4[0] == 10 || | ||||||
|  | 			(ip4[0] == 172 && ip4[1]&0xf0 == 16) || | ||||||
|  | 			(ip4[0] == 192 && ip4[1] == 168) | ||||||
|  | 	} | ||||||
|  | 	return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc | ||||||
|  | } | ||||||
| @@ -19,6 +19,7 @@ import ( | |||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
|  | 	"syscall" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| @@ -29,6 +30,8 @@ import ( | |||||||
| 	"github.com/gobwas/glob" | 	"github.com/gobwas/glob" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | var contextKeyWebhookRequest interface{} = "contextKeyWebhookRequest" | ||||||
|  |  | ||||||
| // Deliver deliver hook task | // Deliver deliver hook task | ||||||
| func Deliver(t *models.HookTask) error { | func Deliver(t *models.HookTask) error { | ||||||
| 	w, err := models.GetWebhookByID(t.HookID) | 	w, err := models.GetWebhookByID(t.HookID) | ||||||
| @@ -171,7 +174,7 @@ func Deliver(t *models.HookTask) error { | |||||||
| 		return fmt.Errorf("Webhook task skipped (webhooks disabled): [%d]", t.ID) | 		return fmt.Errorf("Webhook task skipped (webhooks disabled): [%d]", t.ID) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	resp, err := webhookHTTPClient.Do(req) | 	resp, err := webhookHTTPClient.Do(req.WithContext(context.WithValue(req.Context(), contextKeyWebhookRequest, req))) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err) | 		t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err) | ||||||
| 		return err | 		return err | ||||||
| @@ -293,14 +296,29 @@ func InitDeliverHooks() { | |||||||
| 	timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second | 	timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second | ||||||
|  |  | ||||||
| 	webhookHTTPClient = &http.Client{ | 	webhookHTTPClient = &http.Client{ | ||||||
|  | 		Timeout: timeout, | ||||||
| 		Transport: &http.Transport{ | 		Transport: &http.Transport{ | ||||||
| 			TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify}, | 			TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify}, | ||||||
| 			Proxy:           webhookProxy(), | 			Proxy:           webhookProxy(), | ||||||
| 			Dial: func(netw, addr string) (net.Conn, error) { | 			DialContext: func(ctx context.Context, network, addrOrHost string) (net.Conn, error) { | ||||||
| 				return net.DialTimeout(netw, addr, timeout) // dial timeout | 				dialer := net.Dialer{ | ||||||
|  | 					Timeout: timeout, | ||||||
|  | 					Control: func(network, ipAddr string, c syscall.RawConn) error { | ||||||
|  | 						// in Control func, the addr was already resolved to IP:PORT format, there is no cost to do ResolveTCPAddr here | ||||||
|  | 						tcpAddr, err := net.ResolveTCPAddr(network, ipAddr) | ||||||
|  | 						req := ctx.Value(contextKeyWebhookRequest).(*http.Request) | ||||||
|  | 						if err != nil { | ||||||
|  | 							return fmt.Errorf("webhook can only call HTTP servers via TCP, deny '%s(%s:%s)', err=%v", req.Host, network, ipAddr, err) | ||||||
|  | 						} | ||||||
|  | 						if !setting.Webhook.AllowedHostList.MatchesHostOrIP(req.Host, tcpAddr.IP) { | ||||||
|  | 							return fmt.Errorf("webhook can only call allowed HTTP servers (check your webhook.ALLOWED_HOST_LIST setting), deny '%s(%s)'", req.Host, ipAddr) | ||||||
|  | 						} | ||||||
|  | 						return nil | ||||||
|  | 					}, | ||||||
|  | 				} | ||||||
|  | 				return dialer.DialContext(ctx, network, addrOrHost) | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 		Timeout: timeout, // request timeout |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	go graceful.GetManager().RunWithShutdownContext(DeliverHooks) | 	go graceful.GetManager().RunWithShutdownContext(DeliverHooks) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user