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