-
-
Notifications
You must be signed in to change notification settings - Fork 4k
/
vhosttrie.go
161 lines (148 loc) · 4.39 KB
/
vhosttrie.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
package httpserver
import (
"net"
"strings"
)
// vhostTrie facilitates virtual hosting. It matches
// requests first by hostname (with support for
// wildcards as TLS certificates support them), then
// by longest matching path.
type vhostTrie struct {
fallbackHosts []string
edges map[string]*vhostTrie
site *SiteConfig // site to match on this node; also known as a virtual host
path string // the path portion of the key for the associated site
}
// newVHostTrie returns a new vhostTrie.
func newVHostTrie() *vhostTrie {
return &vhostTrie{edges: make(map[string]*vhostTrie), fallbackHosts: []string{"0.0.0.0", ""}}
}
// Insert adds stack to t keyed by key. The key should be
// a valid "host/path" combination (or just host).
func (t *vhostTrie) Insert(key string, site *SiteConfig) {
host, path := t.splitHostPath(key)
if _, ok := t.edges[host]; !ok {
t.edges[host] = newVHostTrie()
}
t.edges[host].insertPath(path, path, site)
}
// insertPath expects t to be a host node (not a root node),
// and inserts site into the t according to remainingPath.
func (t *vhostTrie) insertPath(remainingPath, originalPath string, site *SiteConfig) {
if remainingPath == "" {
t.site = site
t.path = originalPath
return
}
ch := string(remainingPath[0])
if _, ok := t.edges[ch]; !ok {
t.edges[ch] = newVHostTrie()
}
t.edges[ch].insertPath(remainingPath[1:], originalPath, site)
}
// Match returns the virtual host (site) in v with
// the closest match to key. If there was a match,
// it returns the SiteConfig and the path portion of
// the key used to make the match. The matched path
// would be a prefix of the path portion of the
// key, if not the whole path portion of the key.
// If there is no match, nil and empty string will
// be returned.
//
// A typical key will be in the form "host" or "host/path".
func (t *vhostTrie) Match(key string) (*SiteConfig, string) {
host, path := t.splitHostPath(key)
// try the given host, then, if no match, try fallback hosts
branch := t.matchHost(host)
for _, h := range t.fallbackHosts {
if branch != nil {
break
}
branch = t.matchHost(h)
}
if branch == nil {
return nil, ""
}
node := branch.matchPath(path)
if node == nil {
return nil, ""
}
return node.site, node.path
}
// matchHost returns the vhostTrie matching host. The matching
// algorithm is the same as used to match certificates to host
// with SNI during TLS handshakes. In other words, it supports,
// to some degree, the use of wildcard (*) characters.
func (t *vhostTrie) matchHost(host string) *vhostTrie {
// try exact match
if subtree, ok := t.edges[host]; ok {
return subtree
}
// then try replacing labels in the host
// with wildcards until we get a match
labels := strings.Split(host, ".")
for i := range labels {
labels[i] = "*"
candidate := strings.Join(labels, ".")
if subtree, ok := t.edges[candidate]; ok {
return subtree
}
}
return nil
}
// matchPath traverses t until it finds the longest key matching
// remainingPath, and returns its node.
func (t *vhostTrie) matchPath(remainingPath string) *vhostTrie {
var longestMatch *vhostTrie
for len(remainingPath) > 0 {
ch := string(remainingPath[0])
next, ok := t.edges[ch]
if !ok {
break
}
if next.site != nil {
longestMatch = next
}
t = next
remainingPath = remainingPath[1:]
}
return longestMatch
}
// splitHostPath separates host from path in key.
func (t *vhostTrie) splitHostPath(key string) (host, path string) {
parts := strings.SplitN(key, "/", 2)
host, path = strings.ToLower(parts[0]), "/"
if len(parts) > 1 {
path += parts[1]
}
// strip out the port (if present) from the host, since
// each port has its own socket, and each socket has its
// own listener, and each listener has its own server
// instance, and each server instance has its own vhosts.
// removing the port is a simple way to standardize so
// when requests come in, we can be sure to get a match.
hostname, _, err := net.SplitHostPort(host)
if err == nil {
host = hostname
}
return
}
// String returns a list of all the entries in t; assumes that
// t is a root node.
func (t *vhostTrie) String() string {
var s string
for host, edge := range t.edges {
s += edge.str(host)
}
return s
}
func (t *vhostTrie) str(prefix string) string {
var s string
for key, edge := range t.edges {
if edge.site != nil {
s += prefix + key + "\n"
}
s += edge.str(prefix + key)
}
return s
}