golang ServerMux自动重定向的问题

ServerMux是golang官方库中提供的路由框架,能够匹配最大路径分别处理到各个handle

简单的一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func newMux() {
mux := http.NewServeMux()
mux.HandleFunc("/v1/", httpV1)
mux.HandleFunc("/v2/", httpV2)
server := &http.Server {
Addr: ":8080",
Handler: mux,
}
if err = server.ListenAndServe();err != nil {
fmt.Println("server port serve failed: %s",err)
return
}
}

func httpV1(wr http.ResponseWriter, r *http.Request) {
fmt.Println(r.URL)
wr.WriteHeader(200)
}

func httpV2(wr http.ResponseWriter, r *http.Request) {
fmt.Println(r.URL)
wr.WriteHeader(200)
}

然而这样的代码,使用curl访问带双斜杠,会返回301,重定向到另外一个路径

对于使用单向加密鉴权(HMAC-SHA1)的代码来说,路径不同会导致鉴权失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
curl -v http://127.0.0.1:8080/v1//Dota
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /v1//Dota HTTP/1.1
> User-Agent: curl/7.35.0
> Host: 127.0.0.1:8080
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Location: /v1/Dota
< Date: Tue, 25 Apr 2017 03:11:54 GMT
< Content-Length: 43
< Content-Type: text/html; charset=utf-8
<
<a href="/v1/Dota">Moved Permanently</a>.

然而并没有执行到httpV1的代码逻辑,就返回了301,这是因为serveMux中对path进行了处理

简单对ServerMux做一个分析

1
2
3
4
5
6
7
8
9
10
11
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
hosts bool // whether any patterns contain hostnames
}

type muxEntry struct {
explicit bool
h Handler
pattern string
}

可以看到这是一个路由表,路径传入后应当就是用这个map来执行对应的handler

例子中的注册函数HandleFunc很简单

1
2
3
4
// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}

而Handle就是实际的handler注册函数

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
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()

//边界条件检测
if pattern == "" {
panic("http: invalid pattern " + pattern)
}
if handler == nil {
panic("http: nil handler")
}
if mux.m[pattern].explicit {
panic("http: multiple registrations for " + pattern)
}

if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}

//是否匹配域名,例如xxx.com/v1
if pattern[0] != '/' {
mux.hosts = true
}

//如果绑定的是/tree/路径,会自动在/tree路径上绑定一个301重定向到/tree/的handler
// Helpful behavior:
// If pattern is /tree/, insert an implicit permanent redirect for /tree.
// It can be overridden by an explicit registration.
n := len(pattern)
if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
// If pattern contains a host name, strip it and use remaining
// path for redirect.
path := pattern
if pattern[0] != '/' {
// In pattern, at least the last character is a '/', so
// strings.Index can't be -1.
path = pattern[strings.Index(pattern, "/"):]
}
url := &url.URL{Path: path}
mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(url.String(), StatusMovedPermanently), pattern: pattern}
}
}

这是绑定handler的逻辑,实际运行的逻辑根据http Handler的封装,在ServeHTTP函数中

1
2
3
4
5
6
7
8
9
10
11
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}

重点来了

1
2
3
4
5
6
7
8
9
10
11
12
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
if r.Method != "CONNECT" {
if p := cleanPath(r.URL.Path); p != r.URL.Path {
_, pattern = mux.handler(r.Host, p)
url := *r.URL
url.Path = p
return RedirectHandler(url.String(), StatusMovedPermanently), pattern
}
}

return mux.handler(r.Host, r.URL.Path)
}

这里的cleanPath会对重复的双//作处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func cleanPath(p string) string {
if p == "" {
return "/"
}
if p[0] != '/' {
p = "/" + p
}
np := path.Clean(p)
// path.Clean removes trailing slash except for root;
// put the trailing slash back if necessary.
if p[len(p)-1] == '/' && np != "/" {
np += "/"
}
return np
}

这里可以总结一些规则

1 如果路径为空,返回/

2 如果路径第一个字符不为/,那么在最前面加一个/

3 根据path库的Clean

3.1. 将连续的多个斜杠替换为单个斜杠

3.2. 剔除每一个.路径名元素(代表当前目录)

例如/tree/.1/2,会变成/tree/2

3.3. 剔除每一个路径内的..路径名元素(代表父目录)和它前面的非..路径名元素

例如/tree/../2,会变成2

3.4. 剔除开始一个根路径的..路径名元素,即将路径开始处的"/.."替换为"/"

例如/../tree,会变成tree

4 如果路径最后一个字符为/且清除后的路径不为/,那么在清除后加上/

例如/tree/在经过Clean函数处理后会变成/tree,此时要补上/


总结

golang对url的路径处理值得学习,目前我遇到的问题有两种解决方案

1 不用serverMux,直接用handler自己来解析路由规则

2 不用HMAC-SHA1加密,对密文做比较的常用思路

用对称加密,反解出密文中的path字段,然后执行mux的clean path来进行对比path值