golang ServerMux自动重定向的问题

原创内容,转载请注明出处

Posted by Weakyon Blog on April 25, 2017

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

简单的一个例子

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)的代码来说,路径不同会导致鉴权失败

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做一个分析

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很简单

// 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注册函数

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函数中

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)
}

重点来了

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会对重复的双//作处理

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值

25 Apr 2017