Skip to content

Commit d897184

Browse files
committed
refactor to modules/hostmatcher
1 parent b5d0fc6 commit d897184

File tree

5 files changed

+107
-93
lines changed

5 files changed

+107
-93
lines changed

modules/hostmatcher/hostmatcher.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package hostmatcher
6+
7+
import (
8+
"net"
9+
"path/filepath"
10+
"strings"
11+
12+
"code.gitea.io/gitea/modules/util"
13+
)
14+
15+
// HostMatchList is used to check if a host or ip is in a list
16+
type HostMatchList struct {
17+
hosts []string
18+
ipNets []*net.IPNet
19+
}
20+
21+
// MatchBuiltinAll all hosts are matched
22+
const MatchBuiltinAll = "all"
23+
24+
// MatchBuiltinExternal A valid non-private unicast IP, all hosts on public internet are matched
25+
const MatchBuiltinExternal = "external"
26+
27+
// 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.
28+
const MatchBuiltinPrivate = "private"
29+
30+
// MatchBuiltinLoopback 127.0.0.0/8 for IPv4 and ::1/128 for IPv6, localhost is included.
31+
const MatchBuiltinLoopback = "loopback"
32+
33+
// ParseHostMatchList parses the host list HostMatchList
34+
func ParseHostMatchList(hostList string) *HostMatchList {
35+
hl := &HostMatchList{}
36+
for _, s := range strings.Split(hostList, ",") {
37+
s = strings.TrimSpace(s)
38+
if s == "" {
39+
continue
40+
}
41+
_, ipNet, err := net.ParseCIDR(s)
42+
if err == nil {
43+
hl.ipNets = append(hl.ipNets, ipNet)
44+
} else {
45+
hl.hosts = append(hl.hosts, s)
46+
}
47+
}
48+
return hl
49+
}
50+
51+
// MatchesHostOrIP checks if the host or IP matches an allow/deny(block) list
52+
func (hl *HostMatchList) MatchesHostOrIP(host string, ip net.IP) bool {
53+
var matched bool
54+
ipStr := ip.String()
55+
loop:
56+
for _, hostInList := range hl.hosts {
57+
switch hostInList {
58+
case "":
59+
continue
60+
case MatchBuiltinAll:
61+
matched = true
62+
break loop
63+
case MatchBuiltinExternal:
64+
if matched = ip.IsGlobalUnicast() && !util.IsIPPrivate(ip); matched {
65+
break loop
66+
}
67+
case MatchBuiltinPrivate:
68+
if matched = util.IsIPPrivate(ip); matched {
69+
break loop
70+
}
71+
case MatchBuiltinLoopback:
72+
if matched = ip.IsLoopback(); matched {
73+
break loop
74+
}
75+
default:
76+
if matched, _ = filepath.Match(hostInList, host); matched {
77+
break loop
78+
}
79+
if matched, _ = filepath.Match(hostInList, ipStr); matched {
80+
break loop
81+
}
82+
}
83+
}
84+
if !matched {
85+
for _, ipNet := range hl.ipNets {
86+
if matched = ipNet.Contains(ip); matched {
87+
break
88+
}
89+
}
90+
}
91+
return matched
92+
}

modules/util/net_test.go renamed to modules/hostmatcher/hostmatcher_test.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a MIT-style
33
// license that can be found in the LICENSE file.
44

5-
package util
5+
package hostmatcher
66

77
import (
88
"net"
@@ -20,7 +20,7 @@ func TestHostOrIPMatchesList(t *testing.T) {
2020

2121
// for IPv6: "::1" is loopback, "fd00::/8" is private
2222

23-
ah, an := ParseHostMatchList("private, external, *.mydomain.com, 169.254.1.0/24")
23+
hl := ParseHostMatchList("private, external, *.mydomain.com, 169.254.1.0/24")
2424
cases := []tc{
2525
{"", net.IPv4zero, false},
2626
{"", net.IPv6zero, false},
@@ -42,10 +42,10 @@ func TestHostOrIPMatchesList(t *testing.T) {
4242
{"", net.ParseIP("169.254.2.2"), false},
4343
}
4444
for _, c := range cases {
45-
assert.Equalf(t, c.expected, HostOrIPMatchesList(c.host, c.ip, ah, an), "case %s(%v)", c.host, c.ip)
45+
assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip)
4646
}
4747

48-
ah, an = ParseHostMatchList("loopback")
48+
hl = ParseHostMatchList("loopback")
4949
cases = []tc{
5050
{"", net.IPv4zero, false},
5151
{"", net.ParseIP("127.0.0.1"), true},
@@ -60,10 +60,10 @@ func TestHostOrIPMatchesList(t *testing.T) {
6060
{"mydomain.com", net.IPv4zero, false},
6161
}
6262
for _, c := range cases {
63-
assert.Equalf(t, c.expected, HostOrIPMatchesList(c.host, c.ip, ah, an), "case %s(%v)", c.host, c.ip)
63+
assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip)
6464
}
6565

66-
ah, an = ParseHostMatchList("private")
66+
hl = ParseHostMatchList("private")
6767
cases = []tc{
6868
{"", net.IPv4zero, false},
6969
{"", net.ParseIP("127.0.0.1"), false},
@@ -78,10 +78,10 @@ func TestHostOrIPMatchesList(t *testing.T) {
7878
{"mydomain.com", net.IPv4zero, false},
7979
}
8080
for _, c := range cases {
81-
assert.Equalf(t, c.expected, HostOrIPMatchesList(c.host, c.ip, ah, an), "case %s(%v)", c.host, c.ip)
81+
assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip)
8282
}
8383

84-
ah, an = ParseHostMatchList("external")
84+
hl = ParseHostMatchList("external")
8585
cases = []tc{
8686
{"", net.IPv4zero, false},
8787
{"", net.ParseIP("127.0.0.1"), false},
@@ -96,10 +96,10 @@ func TestHostOrIPMatchesList(t *testing.T) {
9696
{"mydomain.com", net.IPv4zero, false},
9797
}
9898
for _, c := range cases {
99-
assert.Equalf(t, c.expected, HostOrIPMatchesList(c.host, c.ip, ah, an), "case %s(%v)", c.host, c.ip)
99+
assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip)
100100
}
101101

102-
ah, an = ParseHostMatchList("all")
102+
hl = ParseHostMatchList("all")
103103
cases = []tc{
104104
{"", net.IPv4zero, true},
105105
{"", net.ParseIP("127.0.0.1"), true},
@@ -114,6 +114,6 @@ func TestHostOrIPMatchesList(t *testing.T) {
114114
{"mydomain.com", net.IPv4zero, true},
115115
}
116116
for _, c := range cases {
117-
assert.Equalf(t, c.expected, HostOrIPMatchesList(c.host, c.ip, ah, an), "case %s(%v)", c.host, c.ip)
117+
assert.Equalf(t, c.expected, hl.MatchesHostOrIP(c.host, c.ip), "case %s(%v)", c.host, c.ip)
118118
}
119119
}

modules/setting/webhook.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55
package setting
66

77
import (
8-
"net"
98
"net/url"
109

10+
"code.gitea.io/gitea/modules/hostmatcher"
1111
"code.gitea.io/gitea/modules/log"
12-
"code.gitea.io/gitea/modules/util"
1312
)
1413

1514
var (
@@ -18,8 +17,7 @@ var (
1817
QueueLength int
1918
DeliverTimeout int
2019
SkipTLSVerify bool
21-
AllowedHostList []string
22-
AllowedHostIPNets []*net.IPNet
20+
AllowedHostList *hostmatcher.HostMatchList
2321
Types []string
2422
PagingNum int
2523
ProxyURL string
@@ -40,7 +38,7 @@ func newWebhookService() {
4038
Webhook.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000)
4139
Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5)
4240
Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool()
43-
Webhook.AllowedHostList, Webhook.AllowedHostIPNets = util.ParseHostMatchList(sec.Key("ALLOWED_HOST_LIST").MustString(util.HostListBuiltinExternal))
41+
Webhook.AllowedHostList = hostmatcher.ParseHostMatchList(sec.Key("ALLOWED_HOST_LIST").MustString(hostmatcher.MatchBuiltinExternal))
4442
Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "matrix", "wechatwork"}
4543
Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10)
4644
Webhook.ProxyURL = sec.Key("PROXY_URL").MustString("")

modules/util/net.go

Lines changed: 0 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,8 @@ package util
66

77
import (
88
"net"
9-
"path/filepath"
10-
"strings"
119
)
1210

13-
// HostListBuiltinAll all hosts are matched
14-
const HostListBuiltinAll = "all"
15-
16-
// HostListBuiltinExternal A valid non-private unicast IP, all hosts on public internet are matched
17-
const HostListBuiltinExternal = "external"
18-
19-
// HostListBuiltinPrivate 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.
20-
const HostListBuiltinPrivate = "private"
21-
22-
// HostListBuiltinLoopback 127.0.0.0/8 for IPv4 and ::1/128 for IPv6, localhost is included.
23-
const HostListBuiltinLoopback = "loopback"
24-
2511
// IsIPPrivate for net.IP.IsPrivate. TODO: replace with `ip.IsPrivate()` if min go version is bumped to 1.17
2612
func IsIPPrivate(ip net.IP) bool {
2713
if ip4 := ip.To4(); ip4 != nil {
@@ -31,63 +17,3 @@ func IsIPPrivate(ip net.IP) bool {
3117
}
3218
return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc
3319
}
34-
35-
// ParseHostMatchList parses the host list for HostOrIPMatchesList
36-
func ParseHostMatchList(hostListStr string) (hostList []string, ipNets []*net.IPNet) {
37-
for _, s := range strings.Split(hostListStr, ",") {
38-
s = strings.TrimSpace(s)
39-
if s == "" {
40-
continue
41-
}
42-
_, ipNet, err := net.ParseCIDR(s)
43-
if err == nil {
44-
ipNets = append(ipNets, ipNet)
45-
} else {
46-
hostList = append(hostList, s)
47-
}
48-
}
49-
return
50-
}
51-
52-
// HostOrIPMatchesList checks if the host or IP matches an allow/deny(block) list
53-
func HostOrIPMatchesList(host string, ip net.IP, hostList []string, ipNets []*net.IPNet) bool {
54-
var matched bool
55-
ipStr := ip.String()
56-
loop:
57-
for _, hostInList := range hostList {
58-
switch hostInList {
59-
case "":
60-
continue
61-
case HostListBuiltinAll:
62-
matched = true
63-
break loop
64-
case HostListBuiltinExternal:
65-
if matched = ip.IsGlobalUnicast() && !IsIPPrivate(ip); matched {
66-
break loop
67-
}
68-
case HostListBuiltinPrivate:
69-
if matched = IsIPPrivate(ip); matched {
70-
break loop
71-
}
72-
case HostListBuiltinLoopback:
73-
if matched = ip.IsLoopback(); matched {
74-
break loop
75-
}
76-
default:
77-
if matched, _ = filepath.Match(hostInList, host); matched {
78-
break loop
79-
}
80-
if matched, _ = filepath.Match(hostInList, ipStr); matched {
81-
break loop
82-
}
83-
}
84-
}
85-
if !matched {
86-
for _, ipNet := range ipNets {
87-
if matched = ipNet.Contains(ip); matched {
88-
break
89-
}
90-
}
91-
}
92-
return matched
93-
}

services/webhook/deliver.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ import (
2727
"code.gitea.io/gitea/modules/log"
2828
"code.gitea.io/gitea/modules/proxy"
2929
"code.gitea.io/gitea/modules/setting"
30-
"code.gitea.io/gitea/modules/util"
31-
3230
"github.com/gobwas/glob"
3331
)
3432

@@ -312,7 +310,7 @@ func InitDeliverHooks() {
312310
if err != nil {
313311
return fmt.Errorf("webhook can only call HTTP servers via TCP, deny '%s(%s:%s)', err=%v", req.Host, network, ipAddr, err)
314312
}
315-
if !util.HostOrIPMatchesList(req.Host, tcpAddr.IP, setting.Webhook.AllowedHostList, setting.Webhook.AllowedHostIPNets) {
313+
if !setting.Webhook.AllowedHostList.MatchesHostOrIP(req.Host, tcpAddr.IP) {
316314
return fmt.Errorf("webhook can only call allowed HTTP servers (check your webhook.ALLOWED_HOST_LIST setting), deny '%s(%s)'", req.Host, ipAddr)
317315
}
318316
return nil

0 commit comments

Comments
 (0)