You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

547 lines
13 KiB

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