Skip to content

Commit d007a94

Browse files
committed
Improve documents, unit tests and comments.
1 parent e9618db commit d007a94

File tree

9 files changed

+233
-164
lines changed

9 files changed

+233
-164
lines changed

cmd/web.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,10 @@ func listen(m http.Handler, handleRedirector bool) error {
194194
listenAddr = net.JoinHostPort(listenAddr, setting.HTTPPort)
195195
}
196196
log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubURL)
197-
log.Info("AppURL: %s", setting.AppURL)
197+
// This can be useful for users, many users do wrong to their config and get strange behaviors behind a reverse-proxy.
198+
// A user may fix the configuration mistake when he sees this log.
199+
// And this is also very helpful to maintainers to provide help to users to resolve their configuration problems.
200+
log.Info("AppURL(ROOT_URL): %s", setting.AppURL)
198201

199202
if setting.LFS.StartServer {
200203
log.Info("LFS server enabled")

custom/conf/app.example.ini

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,8 +1396,11 @@ PATH =
13961396
;; Deliver timeout in seconds
13971397
;DELIVER_TIMEOUT = 5
13981398
;;
1399-
;; Webhook can only call allowed hosts for security reasons. Comma separated list: loopback, private, global, or all, or CIDR list (1.2.3.0/8), or wildcard hosts (*.mydomain.com)
1400-
; ALLOWED_HOST_LIST = global
1399+
;; Webhook can only call allowed hosts for security reasons. Comma separated list, eg: external, 192.168.1.0/24, *.mydomain.com
1400+
;; Built-in: loopback (for localhost), private (for LAN/intranet), external (for public hosts on internet), all (for all hosts)
1401+
;; CIDR list: 1.2.3.0/8, 2001:db8::/32
1402+
;; Wildcard hosts: *.mydomain.com, 192.168.100.*
1403+
; ALLOWED_HOST_LIST = external
14011404
;;
14021405
;; Allow insecure certification
14031406
;SKIP_TLS_VERIFY = false

docs/content/doc/advanced/config-cheat-sheet.en-us.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,14 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type
581581

582582
- `QUEUE_LENGTH`: **1000**: Hook task queue length. Use caution when editing this value.
583583
- `DELIVER_TIMEOUT`: **5**: Delivery timeout (sec) for shooting webhooks.
584-
- `ALLOWED_HOST_LIST`: **global**: Webhook can only call allowed hosts for security reasons. Comma separated list: `loopback`, `private`, `global`, or `all`, or CIDR list (1.2.3.0/8), or wildcard hosts (*.mydomain.com)
584+
- `ALLOWED_HOST_LIST`: **external**: Webhook can only call allowed hosts for security reasons. Comma separated list.
585+
- Built-in networks:
586+
- `loopback`: 127.0.0.0/8 for IPv4 and ::1/128 for IPv6, localhost is included.
587+
- `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.
588+
- `external`: A valid non-private unicast IP, you can access all hosts on public internet.
589+
- `all`: All hosts are allowed.
590+
- CIDR list: `1.2.3.0/8` for IPv4 and `2001:db8::/32` for IPv6
591+
- Wildcard hosts: `*.mydomain.com`, `192.168.100.*`
585592
- `SKIP_TLS_VERIFY`: **false**: Allow insecure certification.
586593
- `PAGING_NUM`: **10**: Number of webhook history events that are shown in one page.
587594
- `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.

modules/setting/webhook.go

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ package setting
77
import (
88
"net"
99
"net/url"
10-
"strings"
1110

1211
"code.gitea.io/gitea/modules/log"
12+
"code.gitea.io/gitea/modules/util"
1313
)
1414

1515
var (
@@ -18,7 +18,7 @@ var (
1818
QueueLength int
1919
DeliverTimeout int
2020
SkipTLSVerify bool
21-
AllowedHostList []string // loopback,private,global, or all, or CIDR list, or wildcard hosts
21+
AllowedHostList []string
2222
AllowedHostIPNets []*net.IPNet
2323
Types []string
2424
PagingNum int
@@ -35,29 +35,12 @@ var (
3535
}
3636
)
3737

38-
// ParseWebhookAllowedHostList parses the ALLOWED_HOST_LIST value
39-
func ParseWebhookAllowedHostList(allowedHostListStr string) (allowedHostList []string, allowedHostIPNets []*net.IPNet) {
40-
for _, s := range strings.Split(allowedHostListStr, ",") {
41-
s = strings.TrimSpace(s)
42-
if s == "" {
43-
continue
44-
}
45-
_, ipNet, err := net.ParseCIDR(s)
46-
if err == nil {
47-
allowedHostIPNets = append(allowedHostIPNets, ipNet)
48-
} else {
49-
allowedHostList = append(allowedHostList, s)
50-
}
51-
}
52-
return
53-
}
54-
5538
func newWebhookService() {
5639
sec := Cfg.Section("webhook")
5740
Webhook.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000)
5841
Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5)
5942
Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool()
60-
Webhook.AllowedHostList, Webhook.AllowedHostIPNets = ParseWebhookAllowedHostList(sec.Key("ALLOWED_HOST_LIST").MustString("global"))
43+
Webhook.AllowedHostList, Webhook.AllowedHostIPNets = util.ParseHostMatchList(sec.Key("ALLOWED_HOST_LIST").MustString(util.HostListBuiltinExternal))
6144
Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "matrix", "wechatwork"}
6245
Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10)
6346
Webhook.ProxyURL = sec.Key("PROXY_URL").MustString("")

modules/util/net.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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 util
6+
7+
import (
8+
"net"
9+
"path/filepath"
10+
"strings"
11+
)
12+
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+
25+
// IsIPPrivate for net.IP.IsPrivate. TODO: replace with `ip.IsPrivate()` if min go version is bumped to 1.17
26+
func IsIPPrivate(ip net.IP) bool {
27+
if ip4 := ip.To4(); ip4 != nil {
28+
return ip4[0] == 10 ||
29+
(ip4[0] == 172 && ip4[1]&0xf0 == 16) ||
30+
(ip4[0] == 192 && ip4[1] == 168)
31+
}
32+
return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc
33+
}
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+
}

modules/util/net_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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 util
6+
7+
import (
8+
"net"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func TestHostOrIPMatchesList(t *testing.T) {
15+
type tc struct {
16+
host string
17+
ip net.IP
18+
expected bool
19+
}
20+
21+
// for IPv6: "::1" is loopback, "fd00::/8" is private
22+
23+
ah, an := ParseHostMatchList("private, external, *.mydomain.com, 169.254.1.0/24")
24+
cases := []tc{
25+
{"", net.IPv4zero, false},
26+
{"", net.IPv6zero, false},
27+
28+
{"", net.ParseIP("127.0.0.1"), false},
29+
{"", net.ParseIP("::1"), false},
30+
31+
{"", net.ParseIP("10.0.1.1"), true},
32+
{"", net.ParseIP("192.168.1.1"), true},
33+
{"", net.ParseIP("fd00::1"), true},
34+
35+
{"", net.ParseIP("8.8.8.8"), true},
36+
{"", net.ParseIP("1001::1"), true},
37+
38+
{"mydomain.com", net.IPv4zero, false},
39+
{"sub.mydomain.com", net.IPv4zero, true},
40+
41+
{"", net.ParseIP("169.254.1.1"), true},
42+
{"", net.ParseIP("169.254.2.2"), false},
43+
}
44+
for _, c := range cases {
45+
assert.Equalf(t, c.expected, HostOrIPMatchesList(c.host, c.ip, ah, an), "case %s(%v)", c.host, c.ip)
46+
}
47+
48+
ah, an = ParseHostMatchList("loopback")
49+
cases = []tc{
50+
{"", net.IPv4zero, false},
51+
{"", net.ParseIP("127.0.0.1"), true},
52+
{"", net.ParseIP("10.0.1.1"), false},
53+
{"", net.ParseIP("192.168.1.1"), false},
54+
{"", net.ParseIP("8.8.8.8"), false},
55+
56+
{"", net.ParseIP("::1"), true},
57+
{"", net.ParseIP("fd00::1"), false},
58+
{"", net.ParseIP("1000::1"), false},
59+
60+
{"mydomain.com", net.IPv4zero, false},
61+
}
62+
for _, c := range cases {
63+
assert.Equalf(t, c.expected, HostOrIPMatchesList(c.host, c.ip, ah, an), "case %s(%v)", c.host, c.ip)
64+
}
65+
66+
ah, an = ParseHostMatchList("private")
67+
cases = []tc{
68+
{"", net.IPv4zero, false},
69+
{"", net.ParseIP("127.0.0.1"), false},
70+
{"", net.ParseIP("10.0.1.1"), true},
71+
{"", net.ParseIP("192.168.1.1"), true},
72+
{"", net.ParseIP("8.8.8.8"), false},
73+
74+
{"", net.ParseIP("::1"), false},
75+
{"", net.ParseIP("fd00::1"), true},
76+
{"", net.ParseIP("1000::1"), false},
77+
78+
{"mydomain.com", net.IPv4zero, false},
79+
}
80+
for _, c := range cases {
81+
assert.Equalf(t, c.expected, HostOrIPMatchesList(c.host, c.ip, ah, an), "case %s(%v)", c.host, c.ip)
82+
}
83+
84+
ah, an = ParseHostMatchList("external")
85+
cases = []tc{
86+
{"", net.IPv4zero, false},
87+
{"", net.ParseIP("127.0.0.1"), false},
88+
{"", net.ParseIP("10.0.1.1"), false},
89+
{"", net.ParseIP("192.168.1.1"), false},
90+
{"", net.ParseIP("8.8.8.8"), true},
91+
92+
{"", net.ParseIP("::1"), false},
93+
{"", net.ParseIP("fd00::1"), false},
94+
{"", net.ParseIP("1000::1"), true},
95+
96+
{"mydomain.com", net.IPv4zero, false},
97+
}
98+
for _, c := range cases {
99+
assert.Equalf(t, c.expected, HostOrIPMatchesList(c.host, c.ip, ah, an), "case %s(%v)", c.host, c.ip)
100+
}
101+
102+
ah, an = ParseHostMatchList("all")
103+
cases = []tc{
104+
{"", net.IPv4zero, true},
105+
{"", net.ParseIP("127.0.0.1"), true},
106+
{"", net.ParseIP("10.0.1.1"), true},
107+
{"", net.ParseIP("192.168.1.1"), true},
108+
{"", net.ParseIP("8.8.8.8"), true},
109+
110+
{"", net.ParseIP("::1"), true},
111+
{"", net.ParseIP("fd00::1"), true},
112+
{"", net.ParseIP("1000::1"), true},
113+
114+
{"mydomain.com", net.IPv4zero, true},
115+
}
116+
for _, c := range cases {
117+
assert.Equalf(t, c.expected, HostOrIPMatchesList(c.host, c.ip, ah, an), "case %s(%v)", c.host, c.ip)
118+
}
119+
}

modules/util/util.go

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"crypto/rand"
1010
"errors"
1111
"math/big"
12-
"net"
1312
"strconv"
1413
"strings"
1514
)
@@ -162,13 +161,3 @@ func RandomString(length int64) (string, error) {
162161
}
163162
return string(bytes), nil
164163
}
165-
166-
// IsIPPrivate for net.IP.IsPrivate. TODO: replace with `ip.IsPrivate()` if min go version is bumped to 1.17
167-
func IsIPPrivate(ip net.IP) bool {
168-
if ip4 := ip.To4(); ip4 != nil {
169-
return ip4[0] == 10 ||
170-
(ip4[0] == 172 && ip4[1]&0xf0 == 16) ||
171-
(ip4[0] == 192 && ip4[1] == 168)
172-
}
173-
return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc
174-
}

services/webhook/deliver.go

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
"net"
1717
"net/http"
1818
"net/url"
19-
"path/filepath"
2019
"strconv"
2120
"strings"
2221
"sync"
@@ -294,51 +293,6 @@ func webhookProxy() func(req *http.Request) (*url.URL, error) {
294293
}
295294
}
296295

297-
func isWebhookRequestAllowed(allowedHostList []string, allowedHostIPNets []*net.IPNet, host string, ip net.IP) bool {
298-
var allowed bool
299-
ipStr := ip.String()
300-
loop:
301-
for _, allowedHost := range allowedHostList {
302-
switch allowedHost {
303-
case "":
304-
continue
305-
case "all":
306-
allowed = true
307-
break loop
308-
case "global":
309-
if allowed = ip.IsGlobalUnicast() && !util.IsIPPrivate(ip); allowed {
310-
break loop
311-
}
312-
case "private":
313-
if allowed = util.IsIPPrivate(ip); allowed {
314-
break loop
315-
}
316-
case "loopback":
317-
if allowed = ip.IsLoopback(); allowed {
318-
break loop
319-
}
320-
default:
321-
if ok, _ := filepath.Match(allowedHost, host); ok {
322-
allowed = true
323-
break loop
324-
}
325-
if ok, _ := filepath.Match(allowedHost, ipStr); ok {
326-
allowed = true
327-
break loop
328-
}
329-
}
330-
}
331-
if !allowed {
332-
for _, allowIPNet := range allowedHostIPNets {
333-
if allowIPNet.Contains(ip) {
334-
allowed = true
335-
break
336-
}
337-
}
338-
}
339-
return allowed
340-
}
341-
342296
// InitDeliverHooks starts the hooks delivery thread
343297
func InitDeliverHooks() {
344298
timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second
@@ -358,7 +312,7 @@ func InitDeliverHooks() {
358312
if err != nil {
359313
return fmt.Errorf("webhook can only call HTTP servers via TCP, deny '%s(%s:%s)', err=%v", req.Host, network, ipAddr, err)
360314
}
361-
if !isWebhookRequestAllowed(setting.Webhook.AllowedHostList, setting.Webhook.AllowedHostIPNets, req.Host, tcpAddr.IP) {
315+
if !util.HostOrIPMatchesList(req.Host, tcpAddr.IP, setting.Webhook.AllowedHostList, setting.Webhook.AllowedHostIPNets) {
362316
return fmt.Errorf("webhook can only call allowed HTTP servers (check your webhook.ALLOWED_HOST_LIST setting), deny '%s(%s)'", req.Host, ipAddr)
363317
}
364318
return nil

0 commit comments

Comments
 (0)