github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/tools/godoc/dl/dl.go (about)

     1  // Copyright 2015 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by the Apache 2.0
     3  // license that can be found in the LICENSE file.
     4  
     5  // +build appengine
     6  
     7  // Package dl implements a simple downloads frontend server.
     8  //
     9  // It accepts HTTP POST requests to create a new download metadata entity, and
    10  // lists entities with sorting and filtering.
    11  // It is designed to run only on the instance of godoc that serves golang.org.
    12  package dl
    13  
    14  import (
    15  	"crypto/hmac"
    16  	"crypto/md5"
    17  	"encoding/json"
    18  	"fmt"
    19  	"html/template"
    20  	"io"
    21  	"net/http"
    22  	"regexp"
    23  	"sort"
    24  	"strconv"
    25  	"strings"
    26  	"sync"
    27  	"time"
    28  
    29  	"golang.org/x/net/context"
    30  
    31  	"google.golang.org/appengine"
    32  	"google.golang.org/appengine/datastore"
    33  	"google.golang.org/appengine/log"
    34  	"google.golang.org/appengine/memcache"
    35  	"google.golang.org/appengine/user"
    36  )
    37  
    38  const (
    39  	gcsBaseURL    = "https://storage.googleapis.com/golang/"
    40  	cacheKey      = "download_list_3" // increment if listTemplateData changes
    41  	cacheDuration = time.Hour
    42  )
    43  
    44  func RegisterHandlers(mux *http.ServeMux) {
    45  	mux.Handle("/dl", http.RedirectHandler("/dl/", http.StatusFound))
    46  	mux.HandleFunc("/dl/", getHandler) // also serves listHandler
    47  	mux.HandleFunc("/dl/upload", uploadHandler)
    48  	mux.HandleFunc("/dl/init", initHandler)
    49  }
    50  
    51  type File struct {
    52  	Filename string
    53  	OS       string
    54  	Arch     string
    55  	Version  string
    56  	Checksum string `datastore:",noindex"`
    57  	Size     int64  `datastore:",noindex"`
    58  	Kind     string // "archive", "installer", "source"
    59  	Uploaded time.Time
    60  }
    61  
    62  func (f File) PrettyOS() string {
    63  	if f.OS == "darwin" {
    64  		switch {
    65  		case strings.Contains(f.Filename, "osx10.8"):
    66  			return "OS X 10.8+"
    67  		case strings.Contains(f.Filename, "osx10.6"):
    68  			return "OS X 10.6+"
    69  		}
    70  	}
    71  	return pretty(f.OS)
    72  }
    73  
    74  func (f File) PrettySize() string {
    75  	const mb = 1 << 20
    76  	if f.Size == 0 {
    77  		return ""
    78  	}
    79  	if f.Size < mb {
    80  		// All Go releases are >1mb, but handle this case anyway.
    81  		return fmt.Sprintf("%v bytes", f.Size)
    82  	}
    83  	return fmt.Sprintf("%.0fMB", float64(f.Size)/mb)
    84  }
    85  
    86  func (f File) Highlight() bool {
    87  	switch {
    88  	case f.Kind == "source":
    89  		return true
    90  	case f.Arch == "amd64" && f.OS == "linux":
    91  		return true
    92  	case f.Arch == "amd64" && f.Kind == "installer":
    93  		switch f.OS {
    94  		case "windows":
    95  			return true
    96  		case "darwin":
    97  			if !strings.Contains(f.Filename, "osx10.6") {
    98  				return true
    99  			}
   100  		}
   101  	}
   102  	return false
   103  }
   104  
   105  func (f File) URL() string {
   106  	return gcsBaseURL + f.Filename
   107  }
   108  
   109  type Release struct {
   110  	Version string
   111  	Stable  bool
   112  	Files   []File
   113  	Visible bool // show files on page load
   114  }
   115  
   116  type Feature struct {
   117  	// The File field will be filled in by the first stable File
   118  	// whose name matches the given fileRE.
   119  	File
   120  	fileRE *regexp.Regexp
   121  
   122  	Platform     string // "Microsoft Windows", "Mac OS X", "Linux"
   123  	Requirements string // "Windows XP and above, 64-bit Intel Processor"
   124  }
   125  
   126  // featuredFiles lists the platforms and files to be featured
   127  // at the top of the downloads page.
   128  var featuredFiles = []Feature{
   129  	{
   130  		Platform:     "Microsoft Windows",
   131  		Requirements: "Windows XP or later, Intel 64-bit processor",
   132  		fileRE:       regexp.MustCompile(`\.windows-amd64\.msi$`),
   133  	},
   134  	{
   135  		Platform:     "Apple OS X",
   136  		Requirements: "OS X 10.8 or later, Intel 64-bit processor",
   137  		fileRE:       regexp.MustCompile(`\.darwin-amd64(-osx10\.8)?\.pkg$`),
   138  	},
   139  	{
   140  		Platform:     "Linux",
   141  		Requirements: "Linux 2.6.23 or later, Intel 64-bit processor",
   142  		fileRE:       regexp.MustCompile(`\.linux-amd64\.tar\.gz$`),
   143  	},
   144  	{
   145  		Platform: "Source",
   146  		fileRE:   regexp.MustCompile(`\.src\.tar\.gz$`),
   147  	},
   148  }
   149  
   150  // data to send to the template; increment cacheKey if you change this.
   151  type listTemplateData struct {
   152  	Featured         []Feature
   153  	Stable, Unstable []Release
   154  	LoginURL         string
   155  }
   156  
   157  var (
   158  	listTemplate  = template.Must(template.New("").Funcs(templateFuncs).Parse(templateHTML))
   159  	templateFuncs = template.FuncMap{"pretty": pretty}
   160  )
   161  
   162  func listHandler(w http.ResponseWriter, r *http.Request) {
   163  	if r.Method != "GET" {
   164  		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
   165  		return
   166  	}
   167  	var (
   168  		c = appengine.NewContext(r)
   169  		d listTemplateData
   170  	)
   171  	if _, err := memcache.Gob.Get(c, cacheKey, &d); err != nil {
   172  		if err == memcache.ErrCacheMiss {
   173  			log.Debugf(c, "cache miss")
   174  		} else {
   175  			log.Errorf(c, "cache get error: %v", err)
   176  		}
   177  
   178  		var fs []File
   179  		_, err := datastore.NewQuery("File").Ancestor(rootKey(c)).GetAll(c, &fs)
   180  		if err != nil {
   181  			log.Errorf(c, "error listing: %v", err)
   182  			return
   183  		}
   184  		d.Stable, d.Unstable = filesToReleases(fs)
   185  		if len(d.Stable) > 0 {
   186  			d.Featured = filesToFeatured(d.Stable[0].Files)
   187  		}
   188  
   189  		d.LoginURL, _ = user.LoginURL(c, "/dl")
   190  		if user.Current(c) != nil {
   191  			d.LoginURL, _ = user.LogoutURL(c, "/dl")
   192  		}
   193  
   194  		item := &memcache.Item{Key: cacheKey, Object: &d, Expiration: cacheDuration}
   195  		if err := memcache.Gob.Set(c, item); err != nil {
   196  			log.Errorf(c, "cache set error: %v", err)
   197  		}
   198  	}
   199  	if err := listTemplate.ExecuteTemplate(w, "root", d); err != nil {
   200  		log.Errorf(c, "error executing template: %v", err)
   201  	}
   202  }
   203  
   204  func filesToFeatured(fs []File) (featured []Feature) {
   205  	for _, feature := range featuredFiles {
   206  		for _, file := range fs {
   207  			if feature.fileRE.MatchString(file.Filename) {
   208  				feature.File = file
   209  				featured = append(featured, feature)
   210  				break
   211  			}
   212  		}
   213  	}
   214  	return
   215  }
   216  
   217  func filesToReleases(fs []File) (stable, unstable []Release) {
   218  	sort.Sort(fileOrder(fs))
   219  
   220  	var r *Release
   221  	var stableMaj, stableMin int
   222  	add := func() {
   223  		if r == nil {
   224  			return
   225  		}
   226  		if r.Stable {
   227  			if len(stable) == 0 {
   228  				// Display files for latest stable release.
   229  				stableMaj, stableMin, _ = parseVersion(r.Version)
   230  				r.Visible = len(stable) == 0
   231  			}
   232  			stable = append(stable, *r)
   233  			return
   234  		}
   235  		if len(unstable) != 0 {
   236  			// Only show one (latest) unstable version.
   237  			return
   238  		}
   239  		maj, min, _ := parseVersion(r.Version)
   240  		if maj < stableMaj || maj == stableMaj && min <= stableMin {
   241  			// Display unstable version only if newer than the
   242  			// latest stable release.
   243  			return
   244  		}
   245  		r.Visible = true
   246  		unstable = append(unstable, *r)
   247  	}
   248  	for _, f := range fs {
   249  		if r == nil || f.Version != r.Version {
   250  			add()
   251  			r = &Release{
   252  				Version: f.Version,
   253  				Stable:  isStable(f.Version),
   254  			}
   255  		}
   256  		r.Files = append(r.Files, f)
   257  	}
   258  	add()
   259  	return
   260  }
   261  
   262  // isStable reports whether the version string v is a stable version.
   263  func isStable(v string) bool {
   264  	return !strings.Contains(v, "beta") && !strings.Contains(v, "rc")
   265  }
   266  
   267  type fileOrder []File
   268  
   269  func (s fileOrder) Len() int      { return len(s) }
   270  func (s fileOrder) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
   271  func (s fileOrder) Less(i, j int) bool {
   272  	a, b := s[i], s[j]
   273  	if av, bv := a.Version, b.Version; av != bv {
   274  		return versionLess(av, bv)
   275  	}
   276  	if a.OS != b.OS {
   277  		return a.OS < b.OS
   278  	}
   279  	if a.Arch != b.Arch {
   280  		return a.Arch < b.Arch
   281  	}
   282  	if a.Kind != b.Kind {
   283  		return a.Kind < b.Kind
   284  	}
   285  	return a.Filename < b.Filename
   286  }
   287  
   288  func versionLess(a, b string) bool {
   289  	// Put stable releases first.
   290  	if isStable(a) != isStable(b) {
   291  		return isStable(a)
   292  	}
   293  	maja, mina, ta := parseVersion(a)
   294  	majb, minb, tb := parseVersion(b)
   295  	if maja == majb {
   296  		if mina == minb {
   297  			return ta >= tb
   298  		}
   299  		return mina >= minb
   300  	}
   301  	return maja >= majb
   302  }
   303  
   304  func parseVersion(v string) (maj, min int, tail string) {
   305  	if i := strings.Index(v, "beta"); i > 0 {
   306  		tail = v[i:]
   307  		v = v[:i]
   308  	}
   309  	if i := strings.Index(v, "rc"); i > 0 {
   310  		tail = v[i:]
   311  		v = v[:i]
   312  	}
   313  	p := strings.Split(strings.TrimPrefix(v, "go1."), ".")
   314  	maj, _ = strconv.Atoi(p[0])
   315  	if len(p) < 2 {
   316  		return
   317  	}
   318  	min, _ = strconv.Atoi(p[1])
   319  	return
   320  }
   321  
   322  func uploadHandler(w http.ResponseWriter, r *http.Request) {
   323  	if r.Method != "POST" {
   324  		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
   325  		return
   326  	}
   327  	c := appengine.NewContext(r)
   328  
   329  	// Authenticate using a user token (same as gomote).
   330  	user := r.FormValue("user")
   331  	if !validUser(user) {
   332  		http.Error(w, "bad user", http.StatusForbidden)
   333  		return
   334  	}
   335  	if r.FormValue("key") != userKey(c, user) {
   336  		http.Error(w, "bad key", http.StatusForbidden)
   337  		return
   338  	}
   339  
   340  	var f File
   341  	defer r.Body.Close()
   342  	if err := json.NewDecoder(r.Body).Decode(&f); err != nil {
   343  		log.Errorf(c, "error decoding upload JSON: %v", err)
   344  		http.Error(w, "Something broke", http.StatusInternalServerError)
   345  		return
   346  	}
   347  	if f.Filename == "" {
   348  		http.Error(w, "Must provide Filename", http.StatusBadRequest)
   349  		return
   350  	}
   351  	if f.Uploaded.IsZero() {
   352  		f.Uploaded = time.Now()
   353  	}
   354  	k := datastore.NewKey(c, "File", f.Filename, 0, rootKey(c))
   355  	if _, err := datastore.Put(c, k, &f); err != nil {
   356  		log.Errorf(c, "putting File entity: %v", err)
   357  		http.Error(w, "could not put File entity", http.StatusInternalServerError)
   358  		return
   359  	}
   360  	if err := memcache.Delete(c, cacheKey); err != nil {
   361  		log.Errorf(c, "cache delete error: %v", err)
   362  	}
   363  	io.WriteString(w, "OK")
   364  }
   365  
   366  func getHandler(w http.ResponseWriter, r *http.Request) {
   367  	name := strings.TrimPrefix(r.URL.Path, "/dl/")
   368  	if name == "" {
   369  		listHandler(w, r)
   370  		return
   371  	}
   372  	if !fileRe.MatchString(name) {
   373  		http.NotFound(w, r)
   374  		return
   375  	}
   376  	http.Redirect(w, r, gcsBaseURL+name, http.StatusFound)
   377  }
   378  
   379  func validUser(user string) bool {
   380  	switch user {
   381  	case "adg", "bradfitz", "cbro":
   382  		return true
   383  	}
   384  	return false
   385  }
   386  
   387  func userKey(c context.Context, user string) string {
   388  	h := hmac.New(md5.New, []byte(secret(c)))
   389  	h.Write([]byte("user-" + user))
   390  	return fmt.Sprintf("%x", h.Sum(nil))
   391  }
   392  
   393  var fileRe = regexp.MustCompile(`^go[0-9a-z.]+\.[0-9a-z.-]+\.(tar\.gz|pkg|msi|zip)$`)
   394  
   395  func initHandler(w http.ResponseWriter, r *http.Request) {
   396  	var fileRoot struct {
   397  		Root string
   398  	}
   399  	c := appengine.NewContext(r)
   400  	k := rootKey(c)
   401  	err := datastore.RunInTransaction(c, func(c context.Context) error {
   402  		err := datastore.Get(c, k, &fileRoot)
   403  		if err != nil && err != datastore.ErrNoSuchEntity {
   404  			return err
   405  		}
   406  		_, err = datastore.Put(c, k, &fileRoot)
   407  		return err
   408  	}, nil)
   409  	if err != nil {
   410  		http.Error(w, err.Error(), 500)
   411  		return
   412  	}
   413  	io.WriteString(w, "OK")
   414  }
   415  
   416  // rootKey is the ancestor of all File entities.
   417  func rootKey(c context.Context) *datastore.Key {
   418  	return datastore.NewKey(c, "FileRoot", "root", 0, nil)
   419  }
   420  
   421  // pretty returns a human-readable version of the given OS, Arch, or Kind.
   422  func pretty(s string) string {
   423  	t, ok := prettyStrings[s]
   424  	if !ok {
   425  		return s
   426  	}
   427  	return t
   428  }
   429  
   430  var prettyStrings = map[string]string{
   431  	"darwin":  "OS X",
   432  	"freebsd": "FreeBSD",
   433  	"linux":   "Linux",
   434  	"windows": "Windows",
   435  
   436  	"386":   "32-bit",
   437  	"amd64": "64-bit",
   438  
   439  	"armv6l": "ARMv6",
   440  
   441  	"archive":   "Archive",
   442  	"installer": "Installer",
   443  	"source":    "Source",
   444  }
   445  
   446  // Code below copied from x/build/app/key
   447  
   448  var theKey struct {
   449  	sync.RWMutex
   450  	builderKey
   451  }
   452  
   453  type builderKey struct {
   454  	Secret string
   455  }
   456  
   457  func (k *builderKey) Key(c context.Context) *datastore.Key {
   458  	return datastore.NewKey(c, "BuilderKey", "root", 0, nil)
   459  }
   460  
   461  func secret(c context.Context) string {
   462  	// check with rlock
   463  	theKey.RLock()
   464  	k := theKey.Secret
   465  	theKey.RUnlock()
   466  	if k != "" {
   467  		return k
   468  	}
   469  
   470  	// prepare to fill; check with lock and keep lock
   471  	theKey.Lock()
   472  	defer theKey.Unlock()
   473  	if theKey.Secret != "" {
   474  		return theKey.Secret
   475  	}
   476  
   477  	// fill
   478  	if err := datastore.Get(c, theKey.Key(c), &theKey.builderKey); err != nil {
   479  		if err == datastore.ErrNoSuchEntity {
   480  			// If the key is not stored in datastore, write it.
   481  			// This only happens at the beginning of a new deployment.
   482  			// The code is left here for SDK use and in case a fresh
   483  			// deployment is ever needed.  "gophers rule" is not the
   484  			// real key.
   485  			if !appengine.IsDevAppServer() {
   486  				panic("lost key from datastore")
   487  			}
   488  			theKey.Secret = "gophers rule"
   489  			datastore.Put(c, theKey.Key(c), &theKey.builderKey)
   490  			return theKey.Secret
   491  		}
   492  		panic("cannot load builder key: " + err.Error())
   493  	}
   494  
   495  	return theKey.Secret
   496  }