6a082708bf
golang.org now has a warning to use pkg.go.dev instead. Use the new URL.
226 lines
6.1 KiB
Go
226 lines
6.1 KiB
Go
// Copyright 2017 Google Inc. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// govanityurls serves Go vanity URLs.
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
type handler struct {
|
|
host string
|
|
cacheControl string
|
|
paths pathConfigSet
|
|
}
|
|
|
|
type pathConfig struct {
|
|
path string
|
|
repo string
|
|
display string
|
|
vcs string
|
|
}
|
|
|
|
func newHandler(config []byte) (*handler, error) {
|
|
var parsed struct {
|
|
Host string `yaml:"host,omitempty"`
|
|
CacheAge *int64 `yaml:"cache_max_age,omitempty"`
|
|
Paths map[string]struct {
|
|
Repo string `yaml:"repo,omitempty"`
|
|
Display string `yaml:"display,omitempty"`
|
|
VCS string `yaml:"vcs,omitempty"`
|
|
} `yaml:"paths,omitempty"`
|
|
}
|
|
if err := yaml.Unmarshal(config, &parsed); err != nil {
|
|
return nil, err
|
|
}
|
|
h := &handler{host: parsed.Host}
|
|
cacheAge := int64(86400) // 24 hours (in seconds)
|
|
if parsed.CacheAge != nil {
|
|
cacheAge = *parsed.CacheAge
|
|
if cacheAge < 0 {
|
|
return nil, errors.New("cache_max_age is negative")
|
|
}
|
|
}
|
|
h.cacheControl = fmt.Sprintf("public, max-age=%d", cacheAge)
|
|
for path, e := range parsed.Paths {
|
|
pc := pathConfig{
|
|
path: strings.TrimSuffix(path, "/"),
|
|
repo: e.Repo,
|
|
display: e.Display,
|
|
vcs: e.VCS,
|
|
}
|
|
switch {
|
|
case e.Display != "":
|
|
// Already filled in.
|
|
case strings.HasPrefix(e.Repo, "https://github.com/"):
|
|
pc.display = fmt.Sprintf("%v %v/tree/master{/dir} %v/blob/master{/dir}/{file}#L{line}", e.Repo, e.Repo, e.Repo)
|
|
case strings.HasPrefix(e.Repo, "https://bitbucket.org"):
|
|
pc.display = fmt.Sprintf("%v %v/src/default{/dir} %v/src/default{/dir}/{file}#{file}-{line}", e.Repo, e.Repo, e.Repo)
|
|
}
|
|
switch {
|
|
case e.VCS != "":
|
|
// Already filled in.
|
|
if e.VCS != "bzr" && e.VCS != "git" && e.VCS != "hg" && e.VCS != "svn" {
|
|
return nil, fmt.Errorf("configuration for %v: unknown VCS %s", path, e.VCS)
|
|
}
|
|
case strings.HasPrefix(e.Repo, "https://github.com/"):
|
|
pc.vcs = "git"
|
|
default:
|
|
return nil, fmt.Errorf("configuration for %v: cannot infer VCS from %s", path, e.Repo)
|
|
}
|
|
h.paths = append(h.paths, pc)
|
|
}
|
|
sort.Sort(h.paths)
|
|
return h, nil
|
|
}
|
|
|
|
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
current := r.URL.Path
|
|
pc, subpath := h.paths.find(current)
|
|
if pc == nil && current == "/" {
|
|
h.serveIndex(w, r)
|
|
return
|
|
}
|
|
if pc == nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Cache-Control", h.cacheControl)
|
|
if err := vanityTmpl.Execute(w, struct {
|
|
Import string
|
|
Subpath string
|
|
Repo string
|
|
Display string
|
|
VCS string
|
|
}{
|
|
Import: h.Host(r) + pc.path,
|
|
Subpath: subpath,
|
|
Repo: pc.repo,
|
|
Display: pc.display,
|
|
VCS: pc.vcs,
|
|
}); err != nil {
|
|
http.Error(w, "cannot render the page", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (h *handler) serveIndex(w http.ResponseWriter, r *http.Request) {
|
|
host := h.Host(r)
|
|
handlers := make([]string, len(h.paths))
|
|
for i, h := range h.paths {
|
|
handlers[i] = host + h.path
|
|
}
|
|
if err := indexTmpl.Execute(w, struct {
|
|
Host string
|
|
Handlers []string
|
|
}{
|
|
Host: host,
|
|
Handlers: handlers,
|
|
}); err != nil {
|
|
http.Error(w, "cannot render the page", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (h *handler) Host(r *http.Request) string {
|
|
host := h.host
|
|
if host == "" {
|
|
host = defaultHost(r)
|
|
}
|
|
return host
|
|
}
|
|
|
|
var indexTmpl = template.Must(template.New("index").Parse(`<!DOCTYPE html>
|
|
<html>
|
|
<h1>{{.Host}}</h1>
|
|
<ul>
|
|
{{range .Handlers}}<li><a href="https://pkg.go.dev/{{.}}">{{.}}</a></li>{{end}}
|
|
</ul>
|
|
</html>
|
|
`))
|
|
|
|
var vanityTmpl = template.Must(template.New("vanity").Parse(`<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
|
<meta name="go-import" content="{{.Import}} {{.VCS}} {{.Repo}}">
|
|
<meta name="go-source" content="{{.Import}} {{.Display}}">
|
|
<meta http-equiv="refresh" content="0; url=https://pkg.go.dev/{{.Import}}/{{.Subpath}}">
|
|
</head>
|
|
<body>
|
|
Nothing to see here; <a href="https://pkg.go.dev/{{.Import}}/{{.Subpath}}">see the package on pkg.go.dev</a>.
|
|
</body>
|
|
</html>`))
|
|
|
|
type pathConfigSet []pathConfig
|
|
|
|
func (pset pathConfigSet) Len() int {
|
|
return len(pset)
|
|
}
|
|
|
|
func (pset pathConfigSet) Less(i, j int) bool {
|
|
return pset[i].path < pset[j].path
|
|
}
|
|
|
|
func (pset pathConfigSet) Swap(i, j int) {
|
|
pset[i], pset[j] = pset[j], pset[i]
|
|
}
|
|
|
|
func (pset pathConfigSet) find(path string) (pc *pathConfig, subpath string) {
|
|
// Fast path with binary search to retrieve exact matches
|
|
// e.g. given pset ["/", "/abc", "/xyz"], path "/def" won't match.
|
|
i := sort.Search(len(pset), func(i int) bool {
|
|
return pset[i].path >= path
|
|
})
|
|
if i < len(pset) && pset[i].path == path {
|
|
return &pset[i], ""
|
|
}
|
|
if i > 0 && strings.HasPrefix(path, pset[i-1].path+"/") {
|
|
return &pset[i-1], path[len(pset[i-1].path)+1:]
|
|
}
|
|
|
|
// Slow path, now looking for the longest prefix/shortest subpath i.e.
|
|
// e.g. given pset ["/", "/abc/", "/abc/def/", "/xyz"/]
|
|
// * query "/abc/foo" returns "/abc/" with a subpath of "foo"
|
|
// * query "/x" returns "/" with a subpath of "x"
|
|
lenShortestSubpath := len(path)
|
|
var bestMatchConfig *pathConfig
|
|
|
|
// After binary search with the >= lexicographic comparison,
|
|
// nothing greater than i will be a prefix of path.
|
|
max := i
|
|
for i := 0; i < max; i++ {
|
|
ps := pset[i]
|
|
if len(ps.path) >= len(path) {
|
|
// We previously didn't find the path by search, so any
|
|
// route with equal or greater length is NOT a match.
|
|
continue
|
|
}
|
|
sSubpath := strings.TrimPrefix(path, ps.path)
|
|
if len(sSubpath) < lenShortestSubpath {
|
|
subpath = sSubpath
|
|
lenShortestSubpath = len(sSubpath)
|
|
bestMatchConfig = &pset[i]
|
|
}
|
|
}
|
|
return bestMatchConfig, subpath
|
|
}
|