|
|
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build appengine
// Package proxy proxies requests to the sandbox compiler service and the
// playground share handler.
// It is designed to run only on the instance of godoc that serves golang.org.
package proxy
import ( "bytes" "crypto/sha1" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/http/httputil" "net/url" "strings" "time"
"golang.org/x/net/context"
"google.golang.org/appengine" "google.golang.org/appengine/log" "google.golang.org/appengine/memcache" "google.golang.org/appengine/urlfetch" )
type Request struct { Body string }
type Response struct { Errors string Events []Event }
type Event struct { Message string Kind string // "stdout" or "stderr"
Delay time.Duration // time to wait before printing Message
}
const ( // We need to use HTTP here for "reasons", but the traffic isn't
// sensitive and it only travels across Google's internal network
// so we should be OK.
sandboxURL = "http://sandbox.golang.org/compile" playgroundURL = "https://play.golang.org" )
const expires = 7 * 24 * time.Hour // 1 week
var cacheControlHeader = fmt.Sprintf("public, max-age=%d", int(expires.Seconds()))
func RegisterHandlers(mux *http.ServeMux) { mux.HandleFunc("/compile", compile) mux.HandleFunc("/share", share) }
func compile(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "I only answer to POST requests.", http.StatusMethodNotAllowed) return }
c := appengine.NewContext(r)
body := r.FormValue("body") res := &Response{} key := cacheKey(body) if _, err := memcache.Gob.Get(c, key, res); err != nil { if err != memcache.ErrCacheMiss { log.Errorf(c, "getting response cache: %v", err) }
req := &Request{Body: body} if err := makeSandboxRequest(c, req, res); err != nil { log.Errorf(c, "compile error: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return }
item := &memcache.Item{Key: key, Object: res} if err := memcache.Gob.Set(c, item); err != nil { log.Errorf(c, "setting response cache: %v", err) } }
expiresTime := time.Now().Add(expires).UTC() w.Header().Set("Expires", expiresTime.Format(time.RFC1123)) w.Header().Set("Cache-Control", cacheControlHeader)
var out interface{} switch r.FormValue("version") { case "2": out = res default: // "1"
out = struct { CompileErrors string `json:"compile_errors"` Output string `json:"output"` }{res.Errors, flatten(res.Events)} } if err := json.NewEncoder(w).Encode(out); err != nil { log.Errorf(c, "encoding response: %v", err) } }
// makeSandboxRequest sends the given Request to the sandbox
// and stores the response in the given Response.
func makeSandboxRequest(c context.Context, req *Request, res *Response) error { reqJ, err := json.Marshal(req) if err != nil { return fmt.Errorf("marshalling request: %v", err) } r, err := urlfetch.Client(c).Post(sandboxURL, "application/json", bytes.NewReader(reqJ)) if err != nil { return fmt.Errorf("making request: %v", err) } defer r.Body.Close() if r.StatusCode != http.StatusOK { b, _ := ioutil.ReadAll(r.Body) return fmt.Errorf("bad status: %v body:\n%s", r.Status, b) } err = json.NewDecoder(r.Body).Decode(res) if err != nil { return fmt.Errorf("unmarshalling response: %v", err) } return nil }
// flatten takes a sequence of Events and returns their contents, concatenated.
func flatten(seq []Event) string { var buf bytes.Buffer for _, e := range seq { buf.WriteString(e.Message) } return buf.String() }
func cacheKey(body string) string { h := sha1.New() io.WriteString(h, body) return fmt.Sprintf("prog-%x", h.Sum(nil)) }
func share(w http.ResponseWriter, r *http.Request) { if googleCN(r) { http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) return } target, _ := url.Parse(playgroundURL) p := httputil.NewSingleHostReverseProxy(target) p.Transport = &urlfetch.Transport{Context: appengine.NewContext(r)} p.ServeHTTP(w, r) }
func googleCN(r *http.Request) bool { if r.FormValue("googlecn") != "" { return true } if appengine.IsDevAppServer() { return false } if strings.HasSuffix(r.Host, ".cn") { return true } switch r.Header.Get("X-AppEngine-Country") { case "", "ZZ", "CN": return true } return false }
|