| // 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 |
| } |