|  | // Copyright 2013 The Go Authors. All rights reserved. | 
|  | // | 
|  | // Use of this source code is governed by a BSD-style | 
|  | // license that can be found in the LICENSE file or at | 
|  | // https://developers.google.com/open-source/licenses/bsd. | 
|  |  | 
|  | package httputil | 
|  |  | 
|  | import ( | 
|  | "io" | 
|  | "io/ioutil" | 
|  | "net/http" | 
|  | "net/url" | 
|  | "strings" | 
|  | "sync" | 
|  | ) | 
|  |  | 
|  | type busterWriter struct { | 
|  | headerMap http.Header | 
|  | status    int | 
|  | io.Writer | 
|  | } | 
|  |  | 
|  | func (bw *busterWriter) Header() http.Header { | 
|  | return bw.headerMap | 
|  | } | 
|  |  | 
|  | func (bw *busterWriter) WriteHeader(status int) { | 
|  | bw.status = status | 
|  | } | 
|  |  | 
|  | // CacheBusters maintains a cache of cache busting tokens for static resources served by Handler. | 
|  | type CacheBusters struct { | 
|  | Handler http.Handler | 
|  |  | 
|  | mu     sync.Mutex | 
|  | tokens map[string]string | 
|  | } | 
|  |  | 
|  | func sanitizeTokenRune(r rune) rune { | 
|  | if r <= ' ' || r >= 127 { | 
|  | return -1 | 
|  | } | 
|  | // Convert percent encoding reserved characters to '-'. | 
|  | if strings.ContainsRune("!#$&'()*+,/:;=?@[]", r) { | 
|  | return '-' | 
|  | } | 
|  | return r | 
|  | } | 
|  |  | 
|  | // Token returns the cache busting token for path. If the token is not already | 
|  | // cached, Get issues a HEAD request on handler and uses the response ETag and | 
|  | // Last-Modified headers to compute a token. | 
|  | func (cb *CacheBusters) Get(path string) string { | 
|  | cb.mu.Lock() | 
|  | if cb.tokens == nil { | 
|  | cb.tokens = make(map[string]string) | 
|  | } | 
|  | token, ok := cb.tokens[path] | 
|  | cb.mu.Unlock() | 
|  | if ok { | 
|  | return token | 
|  | } | 
|  |  | 
|  | w := busterWriter{ | 
|  | Writer:    ioutil.Discard, | 
|  | headerMap: make(http.Header), | 
|  | } | 
|  | r := &http.Request{URL: &url.URL{Path: path}, Method: "HEAD"} | 
|  | cb.Handler.ServeHTTP(&w, r) | 
|  |  | 
|  | if w.status == 200 { | 
|  | token = w.headerMap.Get("Etag") | 
|  | if token == "" { | 
|  | token = w.headerMap.Get("Last-Modified") | 
|  | } | 
|  | token = strings.Trim(token, `" `) | 
|  | token = strings.Map(sanitizeTokenRune, token) | 
|  | } | 
|  |  | 
|  | cb.mu.Lock() | 
|  | cb.tokens[path] = token | 
|  | cb.mu.Unlock() | 
|  |  | 
|  | return token | 
|  | } | 
|  |  | 
|  | // AppendQueryParam appends the token as a query parameter to path. | 
|  | func (cb *CacheBusters) AppendQueryParam(path string, name string) string { | 
|  | token := cb.Get(path) | 
|  | if token == "" { | 
|  | return path | 
|  | } | 
|  | return path + "?" + name + "=" + token | 
|  | } |