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.

402 lines
9.6 KiB

  1. // Copyright 2014 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. // Command tip is the tip.golang.org server,
  5. // serving the latest HEAD straight from the Git oven.
  6. package main
  7. import (
  8. "bufio"
  9. "encoding/json"
  10. "errors"
  11. "flag"
  12. "fmt"
  13. "io"
  14. "io/ioutil"
  15. "log"
  16. "net/http"
  17. "net/http/httputil"
  18. "net/url"
  19. "os"
  20. "os/exec"
  21. "path/filepath"
  22. "sync"
  23. "time"
  24. )
  25. const (
  26. repoURL = "https://go.googlesource.com/"
  27. metaURL = "https://go.googlesource.com/?b=master&format=JSON"
  28. startTimeout = 10 * time.Minute
  29. )
  30. var startTime = time.Now()
  31. var (
  32. autoCertDomain = flag.String("autocert", "", "if non-empty, listen on port 443 and serve a LetsEncrypt cert for this hostname")
  33. autoCertCacheBucket = flag.String("autocert-bucket", "", "if non-empty, the Google Cloud Storage bucket in which to store the LetsEncrypt cache")
  34. )
  35. // runHTTPS, if non-nil, specifies the function to serve HTTPS.
  36. // It is set non-nil in cert.go with the "autocert" build tag.
  37. var runHTTPS func(http.Handler) error
  38. func main() {
  39. flag.Parse()
  40. const k = "TIP_BUILDER"
  41. var b Builder
  42. switch os.Getenv(k) {
  43. case "godoc":
  44. b = godocBuilder{}
  45. case "talks":
  46. b = talksBuilder{}
  47. default:
  48. log.Fatalf("Unknown %v value: %q", k, os.Getenv(k))
  49. }
  50. p := &Proxy{builder: b}
  51. go p.run()
  52. mux := newServeMux(p)
  53. log.Printf("Starting up tip server for builder %q", os.Getenv(k))
  54. errc := make(chan error, 1)
  55. go func() {
  56. errc <- http.ListenAndServe(":8080", mux)
  57. }()
  58. if *autoCertDomain != "" {
  59. if runHTTPS == nil {
  60. errc <- errors.New("can't use --autocert without building binary with the autocert build tag")
  61. } else {
  62. go func() {
  63. errc <- runHTTPS(mux)
  64. }()
  65. }
  66. log.Printf("Listening on port 443 with LetsEncrypt support on domain %q", *autoCertDomain)
  67. }
  68. if err := <-errc; err != nil {
  69. p.stop()
  70. log.Fatal(err)
  71. }
  72. }
  73. // Proxy implements the tip.golang.org server: a reverse-proxy
  74. // that builds and runs godoc instances showing the latest docs.
  75. type Proxy struct {
  76. builder Builder
  77. mu sync.Mutex // protects the followin'
  78. proxy http.Handler
  79. cur string // signature of gorepo+toolsrepo
  80. cmd *exec.Cmd // live godoc instance, or nil for none
  81. side string
  82. hostport string // host and port of the live instance
  83. err error
  84. }
  85. type Builder interface {
  86. Signature(heads map[string]string) string
  87. Init(dir, hostport string, heads map[string]string) (*exec.Cmd, error)
  88. HealthCheck(hostport string) error
  89. }
  90. func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  91. if r.URL.Path == "/_tipstatus" {
  92. p.serveStatus(w, r)
  93. return
  94. }
  95. p.mu.Lock()
  96. proxy := p.proxy
  97. err := p.err
  98. p.mu.Unlock()
  99. if proxy == nil {
  100. s := "starting up"
  101. if err != nil {
  102. s = err.Error()
  103. }
  104. http.Error(w, s, http.StatusInternalServerError)
  105. return
  106. }
  107. proxy.ServeHTTP(w, r)
  108. }
  109. func (p *Proxy) serveStatus(w http.ResponseWriter, r *http.Request) {
  110. p.mu.Lock()
  111. defer p.mu.Unlock()
  112. fmt.Fprintf(w, "side=%v\ncurrent=%v\nerror=%v\nuptime=%v\n", p.side, p.cur, p.err, int(time.Since(startTime).Seconds()))
  113. }
  114. func (p *Proxy) serveHealthCheck(w http.ResponseWriter, r *http.Request) {
  115. p.mu.Lock()
  116. defer p.mu.Unlock()
  117. // NOTE: (App Engine only; not GKE) Status 502, 503, 504 are
  118. // the only status codes that signify an unhealthy app. So
  119. // long as this handler returns one of those codes, this
  120. // instance will not be sent any requests.
  121. if p.proxy == nil {
  122. log.Printf("Health check: not ready")
  123. http.Error(w, "Not ready", http.StatusServiceUnavailable)
  124. return
  125. }
  126. if err := p.builder.HealthCheck(p.hostport); err != nil {
  127. log.Printf("Health check failed: %v", err)
  128. http.Error(w, "Health check failed", http.StatusServiceUnavailable)
  129. return
  130. }
  131. io.WriteString(w, "ok")
  132. }
  133. // run runs in its own goroutine.
  134. func (p *Proxy) run() {
  135. p.side = "a"
  136. for {
  137. p.poll()
  138. time.Sleep(30 * time.Second)
  139. }
  140. }
  141. func (p *Proxy) stop() {
  142. p.mu.Lock()
  143. defer p.mu.Unlock()
  144. if p.cmd != nil {
  145. p.cmd.Process.Kill()
  146. }
  147. }
  148. // poll runs from the run loop goroutine.
  149. func (p *Proxy) poll() {
  150. heads := gerritMetaMap()
  151. if heads == nil {
  152. return
  153. }
  154. sig := p.builder.Signature(heads)
  155. p.mu.Lock()
  156. changes := sig != p.cur
  157. curSide := p.side
  158. p.cur = sig
  159. p.mu.Unlock()
  160. if !changes {
  161. return
  162. }
  163. newSide := "b"
  164. if curSide == "b" {
  165. newSide = "a"
  166. }
  167. dir := filepath.Join(os.TempDir(), "tip", newSide)
  168. if err := os.MkdirAll(dir, 0755); err != nil {
  169. p.err = err
  170. return
  171. }
  172. hostport := "localhost:8081"
  173. if newSide == "b" {
  174. hostport = "localhost:8082"
  175. }
  176. cmd, err := p.builder.Init(dir, hostport, heads)
  177. if err != nil {
  178. err = fmt.Errorf("builder.Init: %v", err)
  179. } else {
  180. go func() {
  181. // TODO(adg,bradfitz): be smarter about dead processes
  182. if err := cmd.Wait(); err != nil {
  183. log.Printf("process in %v exited: %v", dir, err)
  184. }
  185. }()
  186. err = waitReady(p.builder, hostport)
  187. if err != nil {
  188. cmd.Process.Kill()
  189. err = fmt.Errorf("waitReady: %v", err)
  190. }
  191. }
  192. p.mu.Lock()
  193. defer p.mu.Unlock()
  194. if err != nil {
  195. log.Println(err)
  196. p.err = err
  197. return
  198. }
  199. u, err := url.Parse(fmt.Sprintf("http://%v/", hostport))
  200. if err != nil {
  201. err = fmt.Errorf("parsing hostport: %v", err)
  202. log.Println(err)
  203. p.err = err
  204. return
  205. }
  206. p.proxy = httputil.NewSingleHostReverseProxy(u)
  207. p.side = newSide
  208. p.hostport = hostport
  209. if p.cmd != nil {
  210. p.cmd.Process.Kill()
  211. }
  212. p.cmd = cmd
  213. }
  214. func newServeMux(p *Proxy) http.Handler {
  215. mux := http.NewServeMux()
  216. mux.Handle("/", httpsOnlyHandler{p})
  217. mux.HandleFunc("/_ah/health", p.serveHealthCheck)
  218. return mux
  219. }
  220. func waitReady(b Builder, hostport string) error {
  221. var err error
  222. deadline := time.Now().Add(startTimeout)
  223. for time.Now().Before(deadline) {
  224. if err = b.HealthCheck(hostport); err == nil {
  225. return nil
  226. }
  227. time.Sleep(time.Second)
  228. }
  229. return fmt.Errorf("timed out waiting for process at %v: %v", hostport, err)
  230. }
  231. func runErr(cmd *exec.Cmd) error {
  232. out, err := cmd.CombinedOutput()
  233. if err != nil {
  234. if len(out) == 0 {
  235. return err
  236. }
  237. return fmt.Errorf("%s\n%v", out, err)
  238. }
  239. return nil
  240. }
  241. func checkout(repo, hash, path string) error {
  242. // Clone git repo if it doesn't exist.
  243. if _, err := os.Stat(filepath.Join(path, ".git")); os.IsNotExist(err) {
  244. if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
  245. return fmt.Errorf("mkdir: %v", err)
  246. }
  247. if err := runErr(exec.Command("git", "clone", repo, path)); err != nil {
  248. return fmt.Errorf("clone: %v", err)
  249. }
  250. } else if err != nil {
  251. return fmt.Errorf("stat .git: %v", err)
  252. }
  253. // Pull down changes and update to hash.
  254. cmd := exec.Command("git", "fetch")
  255. cmd.Dir = path
  256. if err := runErr(cmd); err != nil {
  257. return fmt.Errorf("fetch: %v", err)
  258. }
  259. cmd = exec.Command("git", "reset", "--hard", hash)
  260. cmd.Dir = path
  261. if err := runErr(cmd); err != nil {
  262. return fmt.Errorf("reset: %v", err)
  263. }
  264. cmd = exec.Command("git", "clean", "-d", "-f", "-x")
  265. cmd.Dir = path
  266. if err := runErr(cmd); err != nil {
  267. return fmt.Errorf("clean: %v", err)
  268. }
  269. return nil
  270. }
  271. var timeoutClient = &http.Client{Timeout: 10 * time.Second}
  272. // gerritMetaMap returns the map from repo name (e.g. "go") to its
  273. // latest master hash.
  274. // The returned map is nil on any transient error.
  275. func gerritMetaMap() map[string]string {
  276. res, err := timeoutClient.Get(metaURL)
  277. if err != nil {
  278. log.Printf("Error getting Gerrit meta map: %v", err)
  279. return nil
  280. }
  281. defer res.Body.Close()
  282. defer io.Copy(ioutil.Discard, res.Body) // ensure EOF for keep-alive
  283. if res.StatusCode != 200 {
  284. return nil
  285. }
  286. var meta map[string]struct {
  287. Branches map[string]string
  288. }
  289. br := bufio.NewReader(res.Body)
  290. // For security reasons or something, this URL starts with ")]}'\n" before
  291. // the JSON object. So ignore that.
  292. // Shawn Pearce says it's guaranteed to always be just one line, ending in '\n'.
  293. for {
  294. b, err := br.ReadByte()
  295. if err != nil {
  296. return nil
  297. }
  298. if b == '\n' {
  299. break
  300. }
  301. }
  302. if err := json.NewDecoder(br).Decode(&meta); err != nil {
  303. log.Printf("JSON decoding error from %v: %s", metaURL, err)
  304. return nil
  305. }
  306. m := map[string]string{}
  307. for repo, v := range meta {
  308. if master, ok := v.Branches["master"]; ok {
  309. m[repo] = master
  310. }
  311. }
  312. return m
  313. }
  314. func getOK(url string) (body []byte, err error) {
  315. res, err := timeoutClient.Get(url)
  316. if err != nil {
  317. return nil, err
  318. }
  319. body, err = ioutil.ReadAll(res.Body)
  320. res.Body.Close()
  321. if err != nil {
  322. return nil, err
  323. }
  324. if res.StatusCode != http.StatusOK {
  325. return nil, errors.New(res.Status)
  326. }
  327. return body, nil
  328. }
  329. // httpsOnlyHandler redirects requests to "http://example.com/foo?bar" to
  330. // "https://example.com/foo?bar". It should be used when the server is listening
  331. // for HTTP traffic behind a proxy that terminates TLS traffic, not when the Go
  332. // server is terminating TLS directly.
  333. type httpsOnlyHandler struct {
  334. h http.Handler
  335. }
  336. // isProxiedReq checks whether the server is running behind a proxy that may be
  337. // terminating TLS.
  338. func isProxiedReq(r *http.Request) bool {
  339. if _, ok := r.Header["X-Appengine-Https"]; ok {
  340. return true
  341. }
  342. if _, ok := r.Header["X-Forwarded-Proto"]; ok {
  343. return true
  344. }
  345. return false
  346. }
  347. func (h httpsOnlyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  348. if r.Header.Get("X-Appengine-Https") == "off" || r.Header.Get("X-Forwarded-Proto") == "http" ||
  349. (!isProxiedReq(r) && r.TLS == nil) {
  350. r.URL.Scheme = "https"
  351. r.URL.Host = r.Host
  352. http.Redirect(w, r, r.URL.String(), http.StatusFound)
  353. return
  354. }
  355. if r.Header.Get("X-Appengine-Https") == "on" || r.Header.Get("X-Forwarded-Proto") == "https" ||
  356. (!isProxiedReq(r) && r.TLS != nil) {
  357. // Only set this header when we're actually in production.
  358. w.Header().Set("Strict-Transport-Security", "max-age=31536000; preload")
  359. }
  360. h.h.ServeHTTP(w, r)
  361. }