-
Notifications
You must be signed in to change notification settings - Fork 5
/
plugin.go
171 lines (148 loc) · 4.5 KB
/
plugin.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
package caddy_remote_host
// heavily inspired by Caddy's remote_ip matcher (https://github.com/caddyserver/caddy/blob/cbb045a121464527d85cce1b56250480b0515f9a/modules/caddyhttp/matchers.go#L123)
import (
"fmt"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/patrickmn/go-cache"
"go.uber.org/zap"
"net"
"net/http"
"regexp"
"strings"
"time"
)
var hostRegex *regexp.Regexp
var cacheKey string = "hosts"
func init() {
caddy.RegisterModule(MatchRemoteHost{})
}
// MatchRemoteHost matches based on the remote IP of the
// connection. A host name can be specified, whose A and AAAA
// DNS records will be resolved to a corresponding IP for matching.
//
// Note that IPs can sometimes be spoofed, so do not rely
// on this as a replacement for actual authentication.
type MatchRemoteHost struct {
// Host names, whose corresponding IPs to match against
Hosts []string `json:"hosts,omitempty"`
// If true, prefer the first IP in the request's X-Forwarded-For
// header, if present, rather than the immediate peer's IP, as
// the reference IP against which to match. Note that it is easy
// to spoof request headers. Default: false
Forwarded bool `json:"forwarded,omitempty"`
// By default, DNS responses are cached for 60 seconds, regardless
// of the DNS record's TTL. Set nocache to true to disable this
// behavior and never use caching. Default: false
NoCache bool `json:"nocache,omitempty"`
logger *zap.Logger
cache *cache.Cache
}
// CaddyModule returns the Caddy module information.
func (MatchRemoteHost) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.matchers.remote_host",
New: func() caddy.Module { return new(MatchRemoteHost) },
}
}
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (m *MatchRemoteHost) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
for d.NextArg() {
if d.Val() == "forwarded" {
if len(m.Hosts) > 0 {
return d.Err("if used, 'forwarded' must appear before 'hosts' argument")
}
m.Forwarded = true
continue
}
if d.Val() == "nocache" {
if len(m.Hosts) > 0 {
return d.Err("if used, 'nocache' must appear before 'hosts' argument")
}
m.NoCache = true
continue
}
m.Hosts = append(m.Hosts, d.Val())
}
if d.NextBlock(0) {
return d.Err("malformed remote_host matcher: blocks are not supported")
}
}
return nil
}
// Provision implements caddy.Provisioner.
func (m *MatchRemoteHost) Provision(ctx caddy.Context) (err error) {
m.logger = ctx.Logger(m)
m.cache = cache.New(1*time.Minute, 2*time.Minute)
hostRegex, err = regexp.Compile(`^((([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))$`)
return err
}
// Validate implements caddy.Validator.
func (m *MatchRemoteHost) Validate() error {
for _, h := range m.Hosts {
if matched := hostRegex.MatchString(h); !matched {
return fmt.Errorf("'%s' is not a valid host name", h)
}
}
return nil
}
// Match returns true if r matches m.
func (m *MatchRemoteHost) Match(r *http.Request) bool {
clientIP, err := m.getClientIP(r)
if err != nil {
m.logger.Error("getting client IP", zap.Error(err))
return false
}
allowedIPs, err := m.resolveIPs()
if err != nil {
m.logger.Error("resolving DNS", zap.Error(err))
return false
}
for _, ip := range allowedIPs {
if ip.Equal(clientIP) {
return true
}
}
return false
}
func (m *MatchRemoteHost) getClientIP(r *http.Request) (net.IP, error) {
remote := r.RemoteAddr
if m.Forwarded {
if fwdFor := r.Header.Get("X-Forwarded-For"); fwdFor != "" {
remote = strings.TrimSpace(strings.Split(fwdFor, ",")[0])
}
}
ipStr, _, err := net.SplitHostPort(remote)
if err != nil {
ipStr = remote
}
ip := net.ParseIP(ipStr)
if ip == nil {
return nil, fmt.Errorf("invalid client IP address: %s", ipStr)
}
return ip, nil
}
func (m *MatchRemoteHost) resolveIPs() ([]net.IP, error) {
if result, ok := m.cache.Get(cacheKey); ok && !m.NoCache {
return result.([]net.IP), nil
}
allIPs := make([]net.IP, 0)
for _, h := range m.Hosts {
ips, err := net.LookupIP(h)
if err != nil {
return nil, err
}
allIPs = append(allIPs, ips...)
}
m.cache.SetDefault(cacheKey, allIPs)
return allIPs, nil
}
// Interface guards
var (
_ caddy.Provisioner = (*MatchRemoteHost)(nil)
_ caddy.Validator = (*MatchRemoteHost)(nil)
_ caddyhttp.RequestMatcher = (*MatchRemoteHost)(nil)
_ caddyfile.Unmarshaler = (*MatchRemoteHost)(nil)
)