github.com/v2fly/tools@v0.100.0/godoc/redirect/redirect.go (about) 1 // Copyright 2013 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package redirect provides hooks to register HTTP handlers that redirect old 6 // godoc paths to their new equivalents and assist in accessing the issue 7 // tracker, wiki, code review system, etc. 8 package redirect // import "github.com/v2fly/tools/godoc/redirect" 9 10 import ( 11 "context" 12 "fmt" 13 "html/template" 14 "net/http" 15 "os" 16 "regexp" 17 "strconv" 18 "strings" 19 "sync" 20 "time" 21 22 "golang.org/x/net/context/ctxhttp" 23 ) 24 25 // Register registers HTTP handlers that redirect old godoc paths to their new 26 // equivalents and assist in accessing the issue tracker, wiki, code review 27 // system, etc. If mux is nil it uses http.DefaultServeMux. 28 func Register(mux *http.ServeMux) { 29 if mux == nil { 30 mux = http.DefaultServeMux 31 } 32 handlePathRedirects(mux, pkgRedirects, "/pkg/") 33 handlePathRedirects(mux, cmdRedirects, "/cmd/") 34 for prefix, redirect := range prefixHelpers { 35 p := "/" + prefix + "/" 36 mux.Handle(p, PrefixHandler(p, redirect)) 37 } 38 for path, redirect := range redirects { 39 mux.Handle(path, Handler(redirect)) 40 } 41 // NB: /src/pkg (sans trailing slash) is the index of packages. 42 mux.HandleFunc("/src/pkg/", srcPkgHandler) 43 mux.HandleFunc("/cl/", clHandler) 44 mux.HandleFunc("/change/", changeHandler) 45 mux.HandleFunc("/design/", designHandler) 46 } 47 48 func handlePathRedirects(mux *http.ServeMux, redirects map[string]string, prefix string) { 49 for source, target := range redirects { 50 h := Handler(prefix + target + "/") 51 p := prefix + source 52 mux.Handle(p, h) 53 mux.Handle(p+"/", h) 54 } 55 } 56 57 // Packages that were renamed between r60 and go1. 58 var pkgRedirects = map[string]string{ 59 "asn1": "encoding/asn1", 60 "big": "math/big", 61 "cmath": "math/cmplx", 62 "csv": "encoding/csv", 63 "exec": "os/exec", 64 "exp/template/html": "html/template", 65 "gob": "encoding/gob", 66 "http": "net/http", 67 "http/cgi": "net/http/cgi", 68 "http/fcgi": "net/http/fcgi", 69 "http/httptest": "net/http/httptest", 70 "http/pprof": "net/http/pprof", 71 "json": "encoding/json", 72 "mail": "net/mail", 73 "rand": "math/rand", 74 "rpc": "net/rpc", 75 "rpc/jsonrpc": "net/rpc/jsonrpc", 76 "scanner": "text/scanner", 77 "smtp": "net/smtp", 78 "tabwriter": "text/tabwriter", 79 "template": "text/template", 80 "template/parse": "text/template/parse", 81 "url": "net/url", 82 "utf16": "unicode/utf16", 83 "utf8": "unicode/utf8", 84 "xml": "encoding/xml", 85 } 86 87 // Commands that were renamed between r60 and go1. 88 var cmdRedirects = map[string]string{ 89 "gofix": "fix", 90 "goinstall": "go", 91 "gopack": "pack", 92 "gotest": "go", 93 "govet": "vet", 94 "goyacc": "yacc", 95 } 96 97 var redirects = map[string]string{ 98 "/blog": "/blog/", 99 "/build": "http://build.golang.org", 100 "/change": "https://go.googlesource.com/go", 101 "/cl": "https://go-review.googlesource.com", 102 "/cmd/godoc/": "https://pkg.go.dev/github.com/v2fly/tools/cmd/godoc", 103 "/issue": "https://github.com/golang/go/issues", 104 "/issue/new": "https://github.com/golang/go/issues/new", 105 "/issues": "https://github.com/golang/go/issues", 106 "/issues/new": "https://github.com/golang/go/issues/new", 107 "/play": "http://play.golang.org", 108 "/design": "https://go.googlesource.com/proposal/+/master/design", 109 110 // In Go 1.2 the references page is part of /doc/. 111 "/ref": "/doc/#references", 112 // This next rule clobbers /ref/spec and /ref/mem. 113 // TODO(adg): figure out what to do here, if anything. 114 // "/ref/": "/doc/#references", 115 116 // Be nice to people who are looking in the wrong place. 117 "/doc/mem": "/ref/mem", 118 "/doc/spec": "/ref/spec", 119 120 "/talks": "http://talks.golang.org", 121 "/tour": "http://tour.golang.org", 122 "/wiki": "https://github.com/golang/go/wiki", 123 124 "/doc/articles/c_go_cgo.html": "/blog/c-go-cgo", 125 "/doc/articles/concurrency_patterns.html": "/blog/go-concurrency-patterns-timing-out-and", 126 "/doc/articles/defer_panic_recover.html": "/blog/defer-panic-and-recover", 127 "/doc/articles/error_handling.html": "/blog/error-handling-and-go", 128 "/doc/articles/gobs_of_data.html": "/blog/gobs-of-data", 129 "/doc/articles/godoc_documenting_go_code.html": "/blog/godoc-documenting-go-code", 130 "/doc/articles/gos_declaration_syntax.html": "/blog/gos-declaration-syntax", 131 "/doc/articles/image_draw.html": "/blog/go-imagedraw-package", 132 "/doc/articles/image_package.html": "/blog/go-image-package", 133 "/doc/articles/json_and_go.html": "/blog/json-and-go", 134 "/doc/articles/json_rpc_tale_of_interfaces.html": "/blog/json-rpc-tale-of-interfaces", 135 "/doc/articles/laws_of_reflection.html": "/blog/laws-of-reflection", 136 "/doc/articles/slices_usage_and_internals.html": "/blog/go-slices-usage-and-internals", 137 "/doc/go_for_cpp_programmers.html": "/wiki/GoForCPPProgrammers", 138 "/doc/go_tutorial.html": "http://tour.golang.org/", 139 } 140 141 var prefixHelpers = map[string]string{ 142 "issue": "https://github.com/golang/go/issues/", 143 "issues": "https://github.com/golang/go/issues/", 144 "play": "http://play.golang.org/", 145 "talks": "http://talks.golang.org/", 146 "wiki": "https://github.com/golang/go/wiki/", 147 } 148 149 func Handler(target string) http.Handler { 150 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 151 url := target 152 if qs := r.URL.RawQuery; qs != "" { 153 url += "?" + qs 154 } 155 http.Redirect(w, r, url, http.StatusMovedPermanently) 156 }) 157 } 158 159 var validID = regexp.MustCompile(`^[A-Za-z0-9-]*/?$`) 160 161 func PrefixHandler(prefix, baseURL string) http.Handler { 162 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 163 if p := r.URL.Path; p == prefix { 164 // redirect /prefix/ to /prefix 165 http.Redirect(w, r, p[:len(p)-1], http.StatusFound) 166 return 167 } 168 id := r.URL.Path[len(prefix):] 169 if !validID.MatchString(id) { 170 http.Error(w, "Not found", http.StatusNotFound) 171 return 172 } 173 target := baseURL + id 174 http.Redirect(w, r, target, http.StatusFound) 175 }) 176 } 177 178 // Redirect requests from the old "/src/pkg/foo" to the new "/src/foo". 179 // See http://golang.org/s/go14nopkg 180 func srcPkgHandler(w http.ResponseWriter, r *http.Request) { 181 r.URL.Path = "/src/" + r.URL.Path[len("/src/pkg/"):] 182 http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently) 183 } 184 185 func clHandler(w http.ResponseWriter, r *http.Request) { 186 const prefix = "/cl/" 187 if p := r.URL.Path; p == prefix { 188 // redirect /prefix/ to /prefix 189 http.Redirect(w, r, p[:len(p)-1], http.StatusFound) 190 return 191 } 192 id := r.URL.Path[len(prefix):] 193 // support /cl/152700045/, which is used in commit 0edafefc36. 194 id = strings.TrimSuffix(id, "/") 195 if !validID.MatchString(id) { 196 http.Error(w, "Not found", http.StatusNotFound) 197 return 198 } 199 target := "" 200 201 if n, err := strconv.Atoi(id); err == nil && isRietveldCL(n) { 202 // Issue 28836: if this Rietveld CL happens to 203 // also be a Gerrit CL, render a disambiguation HTML 204 // page with two links instead. We need to make a 205 // Gerrit API call to figure that out, but we cache 206 // known Gerrit CLs so it's done at most once per CL. 207 if ok, err := isGerritCL(r.Context(), n); err == nil && ok { 208 w.Header().Set("Content-Type", "text/html; charset=utf-8") 209 clDisambiguationHTML.Execute(w, n) 210 return 211 } 212 213 target = "https://codereview.appspot.com/" + id 214 } else { 215 target = "https://go-review.googlesource.com/" + id 216 } 217 http.Redirect(w, r, target, http.StatusFound) 218 } 219 220 var clDisambiguationHTML = template.Must(template.New("").Parse(`<!DOCTYPE html> 221 <html lang="en"> 222 <head> 223 <title>Go CL {{.}} Disambiguation</title> 224 <meta name="viewport" content="width=device-width"> 225 </head> 226 <body> 227 CL number {{.}} exists in both Gerrit (the current code review system) 228 and Rietveld (the previous code review system). Please make a choice: 229 230 <ul> 231 <li><a href="https://go-review.googlesource.com/{{.}}">Gerrit CL {{.}}</a></li> 232 <li><a href="https://codereview.appspot.com/{{.}}">Rietveld CL {{.}}</a></li> 233 </ul> 234 </body> 235 </html>`)) 236 237 // isGerritCL reports whether a Gerrit CL with the specified numeric change ID (e.g., "4247") 238 // is known to exist by querying the Gerrit API at https://go-review.googlesource.com. 239 // isGerritCL uses gerritCLCache as a cache of Gerrit CL IDs that exist. 240 func isGerritCL(ctx context.Context, id int) (bool, error) { 241 // Check cache first. 242 gerritCLCache.Lock() 243 ok := gerritCLCache.exist[id] 244 gerritCLCache.Unlock() 245 if ok { 246 return true, nil 247 } 248 249 // Query the Gerrit API Get Change endpoint, as documented at 250 // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-change. 251 ctx, cancel := context.WithTimeout(ctx, 5*time.Second) 252 defer cancel() 253 resp, err := ctxhttp.Get(ctx, nil, fmt.Sprintf("https://go-review.googlesource.com/changes/%d", id)) 254 if err != nil { 255 return false, err 256 } 257 resp.Body.Close() 258 switch resp.StatusCode { 259 case http.StatusOK: 260 // A Gerrit CL with this ID exists. Add it to cache. 261 gerritCLCache.Lock() 262 gerritCLCache.exist[id] = true 263 gerritCLCache.Unlock() 264 return true, nil 265 case http.StatusNotFound: 266 // A Gerrit CL with this ID doesn't exist. It may get created in the future. 267 return false, nil 268 default: 269 return false, fmt.Errorf("unexpected status code: %v", resp.Status) 270 } 271 } 272 273 var gerritCLCache = struct { 274 sync.Mutex 275 exist map[int]bool // exist is a set of Gerrit CL IDs that are known to exist. 276 }{exist: make(map[int]bool)} 277 278 var changeMap *hashMap 279 280 // LoadChangeMap loads the specified map of Mercurial to Git revisions, 281 // which is used by the /change/ handler to intelligently map old hg 282 // revisions to their new git equivalents. 283 // It should be called before calling Register. 284 // The file should remain open as long as the process is running. 285 // See the implementation of this package for details. 286 func LoadChangeMap(filename string) error { 287 f, err := os.Open(filename) 288 if err != nil { 289 return err 290 } 291 m, err := newHashMap(f) 292 if err != nil { 293 return err 294 } 295 changeMap = m 296 return nil 297 } 298 299 func changeHandler(w http.ResponseWriter, r *http.Request) { 300 const prefix = "/change/" 301 if p := r.URL.Path; p == prefix { 302 // redirect /prefix/ to /prefix 303 http.Redirect(w, r, p[:len(p)-1], http.StatusFound) 304 return 305 } 306 hash := r.URL.Path[len(prefix):] 307 target := "https://go.googlesource.com/go/+/" + hash 308 if git := changeMap.Lookup(hash); git > 0 { 309 target = fmt.Sprintf("https://go.googlesource.com/%v/+/%v", git.Repo(), git.Hash()) 310 } 311 http.Redirect(w, r, target, http.StatusFound) 312 } 313 314 func designHandler(w http.ResponseWriter, r *http.Request) { 315 const prefix = "/design/" 316 if p := r.URL.Path; p == prefix { 317 // redirect /prefix/ to /prefix 318 http.Redirect(w, r, p[:len(p)-1], http.StatusFound) 319 return 320 } 321 name := r.URL.Path[len(prefix):] 322 target := "https://go.googlesource.com/proposal/+/master/design/" + name + ".md" 323 http.Redirect(w, r, target, http.StatusFound) 324 }