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 }